Nextjs App Router Patterns

Master Next.js 14+ App Router with Server Components and streaming

✨ The solution you've been looking for

Verified
Tested and verified by our team
25450 Stars

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.

nextjs app-router server-components react streaming ssr full-stack typescript
Repository

See It In Action

Interactive preview & real-world examples

Live Demo
Skill Demo Animation

AI Conversation Simulator

See how users interact with this skill

User Prompt

Show me how to build a product listing page using Next.js App Router with Server Components that fetch data from an API

Skill Processing

Analyzing request...

Agent Response

Complete implementation with async components, proper data fetching patterns, and Suspense boundaries for streaming

Quick Start (3 Steps)

Get up and running in minutes

1

Install

claude-code skill install nextjs-app-router-patterns

claude-code skill install nextjs-app-router-patterns
2

Config

3

First Trigger

@nextjs-app-router-patterns help

Commands

CommandDescriptionRequired Args
@nextjs-app-router-patterns building-server-components-with-data-fetchingCreate efficient server-side rendered components that fetch data at render timeNone
@nextjs-app-router-patterns implementing-parallel-routes-for-complex-uisSet up independent loading states for different sections of a dashboardNone
@nextjs-app-router-patterns server-actions-for-form-handlingHandle form submissions and mutations using progressive enhancementNone

Typical Use Cases

Building Server Components with Data Fetching

Create efficient server-side rendered components that fetch data at render time

Implementing Parallel Routes for Complex UIs

Set up independent loading states for different sections of a dashboard

Server Actions for Form Handling

Handle form submissions and mutations using progressive enhancement

Overview

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

ModeWhereWhen to Use
Server ComponentsServer onlyData fetching, heavy computation, secrets
Client ComponentsBrowserInteractivity, hooks, browser APIs
StaticBuild timeContent that rarely changes
DynamicRequest timePersonalized or real-time data
StreamingProgressiveLarge 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

 1// app/layout.tsx
 2import { Inter } from 'next/font/google'
 3import { Providers } from './providers'
 4
 5const inter = Inter({ subsets: ['latin'] })
 6
 7export const metadata = {
 8  title: { default: 'My App', template: '%s | My App' },
 9  description: 'Built with Next.js App Router',
10}
11
12export default function RootLayout({
13  children,
14}: {
15  children: React.ReactNode
16}) {
17  return (
18    <html lang="en" suppressHydrationWarning>
19      <body className={inter.className}>
20        <Providers>{children}</Providers>
21      </body>
22    </html>
23  )
24}
25
26// app/page.tsx - Server Component by default
27async function getProducts() {
28  const res = await fetch('https://api.example.com/products', {
29    next: { revalidate: 3600 }, // ISR: revalidate every hour
30  })
31  return res.json()
32}
33
34export default async function HomePage() {
35  const products = await getProducts()
36
37  return (
38    <main>
39      <h1>Products</h1>
40      <ProductGrid products={products} />
41    </main>
42  )
43}

Patterns

Pattern 1: Server Components with Data Fetching

 1// app/products/page.tsx
 2import { Suspense } from 'react'
 3import { ProductList, ProductListSkeleton } from '@/components/products'
 4import { FilterSidebar } from '@/components/filters'
 5
 6interface SearchParams {
 7  category?: string
 8  sort?: 'price' | 'name' | 'date'
 9  page?: string
10}
11
12export default async function ProductsPage({
13  searchParams,
14}: {
15  searchParams: Promise<SearchParams>
16}) {
17  const params = await searchParams
18
19  return (
20    <div className="flex gap-8">
21      <FilterSidebar />
22      <Suspense
23        key={JSON.stringify(params)}
24        fallback={<ProductListSkeleton />}
25      >
26        <ProductList
27          category={params.category}
28          sort={params.sort}
29          page={Number(params.page) || 1}
30        />
31      </Suspense>
32    </div>
33  )
34}
35
36// components/products/ProductList.tsx - Server Component
37async function getProducts(filters: ProductFilters) {
38  const res = await fetch(
39    `${process.env.API_URL}/products?${new URLSearchParams(filters)}`,
40    { next: { tags: ['products'] } }
41  )
42  if (!res.ok) throw new Error('Failed to fetch products')
43  return res.json()
44}
45
46export async function ProductList({ category, sort, page }: ProductFilters) {
47  const { products, totalPages } = await getProducts({ category, sort, page })
48
49  return (
50    <div>
51      <div className="grid grid-cols-3 gap-4">
52        {products.map((product) => (
53          <ProductCard key={product.id} product={product} />
54        ))}
55      </div>
56      <Pagination currentPage={page} totalPages={totalPages} />
57    </div>
58  )
59}

Pattern 2: Client Components with ‘use client’

 1// components/products/AddToCartButton.tsx
 2'use client'
 3
 4import { useState, useTransition } from 'react'
 5import { addToCart } from '@/app/actions/cart'
 6
 7export function AddToCartButton({ productId }: { productId: string }) {
 8  const [isPending, startTransition] = useTransition()
 9  const [error, setError] = useState<string | null>(null)
10
11  const handleClick = () => {
12    setError(null)
13    startTransition(async () => {
14      const result = await addToCart(productId)
15      if (result.error) {
16        setError(result.error)
17      }
18    })
19  }
20
21  return (
22    <div>
23      <button
24        onClick={handleClick}
25        disabled={isPending}
26        className="btn-primary"
27      >
28        {isPending ? 'Adding...' : 'Add to Cart'}
29      </button>
30      {error && <p className="text-red-500 text-sm">{error}</p>}
31    </div>
32  )
33}

Pattern 3: Server Actions

 1// app/actions/cart.ts
 2"use server";
 3
 4import { revalidateTag } from "next/cache";
 5import { cookies } from "next/headers";
 6import { redirect } from "next/navigation";
 7
 8export async function addToCart(productId: string) {
 9  const cookieStore = await cookies();
10  const sessionId = cookieStore.get("session")?.value;
11
12  if (!sessionId) {
13    redirect("/login");
14  }
15
16  try {
17    await db.cart.upsert({
18      where: { sessionId_productId: { sessionId, productId } },
19      update: { quantity: { increment: 1 } },
20      create: { sessionId, productId, quantity: 1 },
21    });
22
23    revalidateTag("cart");
24    return { success: true };
25  } catch (error) {
26    return { error: "Failed to add item to cart" };
27  }
28}
29
30export async function checkout(formData: FormData) {
31  const address = formData.get("address") as string;
32  const payment = formData.get("payment") as string;
33
34  // Validate
35  if (!address || !payment) {
36    return { error: "Missing required fields" };
37  }
38
39  // Process order
40  const order = await processOrder({ address, payment });
41
42  // Redirect to confirmation
43  redirect(`/orders/${order.id}/confirmation`);
44}

Pattern 4: Parallel Routes

 1// app/dashboard/layout.tsx
 2export default function DashboardLayout({
 3  children,
 4  analytics,
 5  team,
 6}: {
 7  children: React.ReactNode
 8  analytics: React.ReactNode
 9  team: React.ReactNode
10}) {
11  return (
12    <div className="dashboard-grid">
13      <main>{children}</main>
14      <aside className="analytics-panel">{analytics}</aside>
15      <aside className="team-panel">{team}</aside>
16    </div>
17  )
18}
19
20// app/dashboard/@analytics/page.tsx
21export default async function AnalyticsSlot() {
22  const stats = await getAnalytics()
23  return <AnalyticsChart data={stats} />
24}
25
26// app/dashboard/@analytics/loading.tsx
27export default function AnalyticsLoading() {
28  return <ChartSkeleton />
29}
30
31// app/dashboard/@team/page.tsx
32export default async function TeamSlot() {
33  const members = await getTeamMembers()
34  return <TeamList members={members} />
35}

Pattern 5: Intercepting Routes (Modal Pattern)

 1// File structure for photo modal
 2// app/
 3// ├── @modal/
 4// │   ├── (.)photos/[id]/page.tsx  # Intercept
 5// │   └── default.tsx
 6// ├── photos/
 7// │   └── [id]/page.tsx            # Full page
 8// └── layout.tsx
 9
10// app/@modal/(.)photos/[id]/page.tsx
11import { Modal } from '@/components/Modal'
12import { PhotoDetail } from '@/components/PhotoDetail'
13
14export default async function PhotoModal({
15  params,
16}: {
17  params: Promise<{ id: string }>
18}) {
19  const { id } = await params
20  const photo = await getPhoto(id)
21
22  return (
23    <Modal>
24      <PhotoDetail photo={photo} />
25    </Modal>
26  )
27}
28
29// app/photos/[id]/page.tsx - Full page version
30export default async function PhotoPage({
31  params,
32}: {
33  params: Promise<{ id: string }>
34}) {
35  const { id } = await params
36  const photo = await getPhoto(id)
37
38  return (
39    <div className="photo-page">
40      <PhotoDetail photo={photo} />
41      <RelatedPhotos photoId={id} />
42    </div>
43  )
44}
45
46// app/layout.tsx
47export default function RootLayout({
48  children,
49  modal,
50}: {
51  children: React.ReactNode
52  modal: React.ReactNode
53}) {
54  return (
55    <html>
56      <body>
57        {children}
58        {modal}
59      </body>
60    </html>
61  )
62}

Pattern 6: Streaming with Suspense

 1// app/product/[id]/page.tsx
 2import { Suspense } from 'react'
 3
 4export default async function ProductPage({
 5  params,
 6}: {
 7  params: Promise<{ id: string }>
 8}) {
 9  const { id } = await params
10
11  // This data loads first (blocking)
12  const product = await getProduct(id)
13
14  return (
15    <div>
16      {/* Immediate render */}
17      <ProductHeader product={product} />
18
19      {/* Stream in reviews */}
20      <Suspense fallback={<ReviewsSkeleton />}>
21        <Reviews productId={id} />
22      </Suspense>
23
24      {/* Stream in recommendations */}
25      <Suspense fallback={<RecommendationsSkeleton />}>
26        <Recommendations productId={id} />
27      </Suspense>
28    </div>
29  )
30}
31
32// These components fetch their own data
33async function Reviews({ productId }: { productId: string }) {
34  const reviews = await getReviews(productId) // Slow API
35  return <ReviewList reviews={reviews} />
36}
37
38async function Recommendations({ productId }: { productId: string }) {
39  const products = await getRecommendations(productId) // ML-based, slow
40  return <ProductCarousel products={products} />
41}

Pattern 7: Route Handlers (API Routes)

 1// app/api/products/route.ts
 2import { NextRequest, NextResponse } from "next/server";
 3
 4export async function GET(request: NextRequest) {
 5  const searchParams = request.nextUrl.searchParams;
 6  const category = searchParams.get("category");
 7
 8  const products = await db.product.findMany({
 9    where: category ? { category } : undefined,
10    take: 20,
11  });
12
13  return NextResponse.json(products);
14}
15
16export async function POST(request: NextRequest) {
17  const body = await request.json();
18
19  const product = await db.product.create({
20    data: body,
21  });
22
23  return NextResponse.json(product, { status: 201 });
24}
25
26// app/api/products/[id]/route.ts
27export async function GET(
28  request: NextRequest,
29  { params }: { params: Promise<{ id: string }> },
30) {
31  const { id } = await params;
32  const product = await db.product.findUnique({ where: { id } });
33
34  if (!product) {
35    return NextResponse.json({ error: "Product not found" }, { status: 404 });
36  }
37
38  return NextResponse.json(product);
39}

Pattern 8: Metadata and SEO

 1// app/products/[slug]/page.tsx
 2import { Metadata } from 'next'
 3import { notFound } from 'next/navigation'
 4
 5type Props = {
 6  params: Promise<{ slug: string }>
 7}
 8
 9export async function generateMetadata({ params }: Props): Promise<Metadata> {
10  const { slug } = await params
11  const product = await getProduct(slug)
12
13  if (!product) return {}
14
15  return {
16    title: product.name,
17    description: product.description,
18    openGraph: {
19      title: product.name,
20      description: product.description,
21      images: [{ url: product.image, width: 1200, height: 630 }],
22    },
23    twitter: {
24      card: 'summary_large_image',
25      title: product.name,
26      description: product.description,
27      images: [product.image],
28    },
29  }
30}
31
32export async function generateStaticParams() {
33  const products = await db.product.findMany({ select: { slug: true } })
34  return products.map((p) => ({ slug: p.slug }))
35}
36
37export default async function ProductPage({ params }: Props) {
38  const { slug } = await params
39  const product = await getProduct(slug)
40
41  if (!product) notFound()
42
43  return <ProductDetail product={product} />
44}

Caching Strategies

Data Cache

 1// No cache (always fresh)
 2fetch(url, { cache: "no-store" });
 3
 4// Cache forever (static)
 5fetch(url, { cache: "force-cache" });
 6
 7// ISR - revalidate after 60 seconds
 8fetch(url, { next: { revalidate: 60 } });
 9
10// Tag-based invalidation
11fetch(url, { next: { tags: ["products"] } });
12
13// Invalidate via Server Action
14("use server");
15import { revalidateTag, revalidatePath } from "next/cache";
16
17export async function updateProduct(id: string, data: ProductData) {
18  await db.product.update({ where: { id }, data });
19  revalidateTag("products");
20  revalidatePath("/products");
21}

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

What Users Are Saying

Real feedback from the community

Environment Matrix

Dependencies

Next.js 14+
React 18+
TypeScript (recommended)

Framework Support

Next.js App Router ✓ (recommended) React Server Components ✓ Tailwind CSS ✓ Prisma ORM ✓

Context Window

Token Usage ~3K-8K tokens for complete patterns with examples

Security & Privacy

Information

Author
wshobson
Updated
2026-01-30
Category
cms-platforms