--- name: nextjs-app-router-patterns description: Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components. --- # Next.js App Router Patterns Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development. ## When to Use This Skill - Building new Next.js applications with App Router - Migrating from Pages Router to App Router - Implementing Server Components and streaming - Setting up parallel and intercepting routes - Optimizing data fetching and caching - Building full-stack features with Server Actions ## Core Concepts ### 1. Rendering Modes | Mode | Where | When to Use | |------|-------|-------------| | **Server Components** | Server only | Data fetching, heavy computation, secrets | | **Client Components** | Browser | Interactivity, hooks, browser APIs | | **Static** | Build time | Content that rarely changes | | **Dynamic** | Request time | Personalized or real-time data | | **Streaming** | Progressive | Large pages, slow data sources | ### 2. File Conventions ``` app/ ├── layout.tsx # Shared UI wrapper ├── page.tsx # Route UI ├── loading.tsx # Loading UI (Suspense) ├── error.tsx # Error boundary ├── not-found.tsx # 404 UI ├── route.ts # API endpoint ├── template.tsx # Re-mounted layout ├── default.tsx # Parallel route fallback └── opengraph-image.tsx # OG image generation ``` ## Quick Start ```typescript // app/layout.tsx import { Inter } from 'next/font/google' import { Providers } from './providers' const inter = Inter({ subsets: ['latin'] }) export const metadata = { title: { default: 'My App', template: '%s | My App' }, description: 'Built with Next.js App Router', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } // app/page.tsx - Server Component by default async function getProducts() { const res = await fetch('https://api.example.com/products', { next: { revalidate: 3600 }, // ISR: revalidate every hour }) return res.json() } export default async function HomePage() { const products = await getProducts() return (

Products

) } ``` ## Patterns ### Pattern 1: Server Components with Data Fetching ```typescript // app/products/page.tsx import { Suspense } from 'react' import { ProductList, ProductListSkeleton } from '@/components/products' import { FilterSidebar } from '@/components/filters' interface SearchParams { category?: string sort?: 'price' | 'name' | 'date' page?: string } export default async function ProductsPage({ searchParams, }: { searchParams: Promise }) { const params = await searchParams return (
} >
) } // components/products/ProductList.tsx - Server Component async function getProducts(filters: ProductFilters) { const res = await fetch( `${process.env.API_URL}/products?${new URLSearchParams(filters)}`, { next: { tags: ['products'] } } ) if (!res.ok) throw new Error('Failed to fetch products') return res.json() } export async function ProductList({ category, sort, page }: ProductFilters) { const { products, totalPages } = await getProducts({ category, sort, page }) return (
{products.map((product) => ( ))}
) } ``` ### Pattern 2: Client Components with 'use client' ```typescript // components/products/AddToCartButton.tsx 'use client' import { useState, useTransition } from 'react' import { addToCart } from '@/app/actions/cart' export function AddToCartButton({ productId }: { productId: string }) { const [isPending, startTransition] = useTransition() const [error, setError] = useState(null) const handleClick = () => { setError(null) startTransition(async () => { const result = await addToCart(productId) if (result.error) { setError(result.error) } }) } return (
{error &&

{error}

}
) } ``` ### Pattern 3: Server Actions ```typescript // app/actions/cart.ts 'use server' import { revalidateTag } from 'next/cache' import { cookies } from 'next/headers' import { redirect } from 'next/navigation' export async function addToCart(productId: string) { const cookieStore = await cookies() const sessionId = cookieStore.get('session')?.value if (!sessionId) { redirect('/login') } try { await db.cart.upsert({ where: { sessionId_productId: { sessionId, productId } }, update: { quantity: { increment: 1 } }, create: { sessionId, productId, quantity: 1 }, }) revalidateTag('cart') return { success: true } } catch (error) { return { error: 'Failed to add item to cart' } } } export async function checkout(formData: FormData) { const address = formData.get('address') as string const payment = formData.get('payment') as string // Validate if (!address || !payment) { return { error: 'Missing required fields' } } // Process order const order = await processOrder({ address, payment }) // Redirect to confirmation redirect(`/orders/${order.id}/confirmation`) } ``` ### Pattern 4: Parallel Routes ```typescript // app/dashboard/layout.tsx export default function DashboardLayout({ children, analytics, team, }: { children: React.ReactNode analytics: React.ReactNode team: React.ReactNode }) { return (
{children}
) } // app/dashboard/@analytics/page.tsx export default async function AnalyticsSlot() { const stats = await getAnalytics() return } // app/dashboard/@analytics/loading.tsx export default function AnalyticsLoading() { return } // app/dashboard/@team/page.tsx export default async function TeamSlot() { const members = await getTeamMembers() return } ``` ### Pattern 5: Intercepting Routes (Modal Pattern) ```typescript // File structure for photo modal // app/ // ├── @modal/ // │ ├── (.)photos/[id]/page.tsx # Intercept // │ └── default.tsx // ├── photos/ // │ └── [id]/page.tsx # Full page // └── layout.tsx // app/@modal/(.)photos/[id]/page.tsx import { Modal } from '@/components/Modal' import { PhotoDetail } from '@/components/PhotoDetail' export default async function PhotoModal({ params, }: { params: Promise<{ id: string }> }) { const { id } = await params const photo = await getPhoto(id) return ( ) } // app/photos/[id]/page.tsx - Full page version export default async function PhotoPage({ params, }: { params: Promise<{ id: string }> }) { const { id } = await params const photo = await getPhoto(id) return (
) } // app/layout.tsx export default function RootLayout({ children, modal, }: { children: React.ReactNode modal: React.ReactNode }) { return ( {children} {modal} ) } ``` ### Pattern 6: Streaming with Suspense ```typescript // app/product/[id]/page.tsx import { Suspense } from 'react' export default async function ProductPage({ params, }: { params: Promise<{ id: string }> }) { const { id } = await params // This data loads first (blocking) const product = await getProduct(id) return (
{/* Immediate render */} {/* Stream in reviews */} }> {/* Stream in recommendations */} }>
) } // These components fetch their own data async function Reviews({ productId }: { productId: string }) { const reviews = await getReviews(productId) // Slow API return } async function Recommendations({ productId }: { productId: string }) { const products = await getRecommendations(productId) // ML-based, slow return } ``` ### Pattern 7: Route Handlers (API Routes) ```typescript // app/api/products/route.ts import { NextRequest, NextResponse } from 'next/server' export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const category = searchParams.get('category') const products = await db.product.findMany({ where: category ? { category } : undefined, take: 20, }) return NextResponse.json(products) } export async function POST(request: NextRequest) { const body = await request.json() const product = await db.product.create({ data: body, }) return NextResponse.json(product, { status: 201 }) } // app/api/products/[id]/route.ts export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params const product = await db.product.findUnique({ where: { id } }) if (!product) { return NextResponse.json( { error: 'Product not found' }, { status: 404 } ) } return NextResponse.json(product) } ``` ### Pattern 8: Metadata and SEO ```typescript // app/products/[slug]/page.tsx import { Metadata } from 'next' import { notFound } from 'next/navigation' type Props = { params: Promise<{ slug: string }> } export async function generateMetadata({ params }: Props): Promise { const { slug } = await params const product = await getProduct(slug) if (!product) return {} return { title: product.name, description: product.description, openGraph: { title: product.name, description: product.description, images: [{ url: product.image, width: 1200, height: 630 }], }, twitter: { card: 'summary_large_image', title: product.name, description: product.description, images: [product.image], }, } } export async function generateStaticParams() { const products = await db.product.findMany({ select: { slug: true } }) return products.map((p) => ({ slug: p.slug })) } export default async function ProductPage({ params }: Props) { const { slug } = await params const product = await getProduct(slug) if (!product) notFound() return } ``` ## Caching Strategies ### Data Cache ```typescript // No cache (always fresh) fetch(url, { cache: 'no-store' }) // Cache forever (static) fetch(url, { cache: 'force-cache' }) // ISR - revalidate after 60 seconds fetch(url, { next: { revalidate: 60 } }) // Tag-based invalidation fetch(url, { next: { tags: ['products'] } }) // Invalidate via Server Action 'use server' import { revalidateTag, revalidatePath } from 'next/cache' export async function updateProduct(id: string, data: ProductData) { await db.product.update({ where: { id }, data }) revalidateTag('products') revalidatePath('/products') } ``` ## Best Practices ### Do's - **Start with Server Components** - Add 'use client' only when needed - **Colocate data fetching** - Fetch data where it's used - **Use Suspense boundaries** - Enable streaming for slow data - **Leverage parallel routes** - Independent loading states - **Use Server Actions** - For mutations with progressive enhancement ### Don'ts - **Don't pass serializable data** - Server → Client boundary limitations - **Don't use hooks in Server Components** - No useState, useEffect - **Don't fetch in Client Components** - Use Server Components or React Query - **Don't over-nest layouts** - Each layout adds to the component tree - **Don't ignore loading states** - Always provide loading.tsx or Suspense ## Resources - [Next.js App Router Documentation](https://nextjs.org/docs/app) - [Server Components RFC](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md) - [Vercel Templates](https://vercel.com/templates/next.js)