Nextjs App Router Patterns
Master Next.js 14+ App Router with Server Components and streaming
✨ The solution you've been looking for
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.
See It In Action
Interactive preview & real-world examples
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
Install
claude-code skill install nextjs-app-router-patterns
claude-code skill install nextjs-app-router-patternsConfig
First Trigger
@nextjs-app-router-patterns helpCommands
| Command | Description | Required Args |
|---|---|---|
| @nextjs-app-router-patterns building-server-components-with-data-fetching | Create efficient server-side rendered components that fetch data at render time | None |
| @nextjs-app-router-patterns implementing-parallel-routes-for-complex-uis | Set up independent loading states for different sections of a dashboard | None |
| @nextjs-app-router-patterns server-actions-for-form-handling | Handle form submissions and mutations using progressive enhancement | None |
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
| 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
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
Framework Support
Context Window
Security & Privacy
Information
- Author
- wshobson
- Updated
- 2026-01-30
- Category
- cms-platforms
Related Skills
Nextjs App Router Patterns
Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data …
View Details →Nextjs 15
Next.js 15 App Router patterns. Trigger: When working in Next.js App Router (app/), Server …
View Details →Nextjs 15
Next.js 15 App Router patterns. Trigger: When working in Next.js App Router (app/), Server …
View Details →