Back to Next.js

nextjs-app-router-patterns

Next.jsApp RouterServer ComponentsReactSSRData FetchingStreamingFull-stack
⭐ 36.8kπŸ“„ MITπŸ•’ 2026-06-16Source β†—

Install this skill

npx skills add wshobson/agents

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

The nextjs-app-router-patterns skill provides a structured framework for architecting applications within the Next.js 14+ ecosystem. It manages the interplay between Server Components for data-heavy operations and Client Components for browser-side interactivity. By strictly following the file-system-based routing logic, it enforces standardized handling for loading states, error boundaries, and nested layout hierarchies. This skill emphasizes the transition from traditional API-heavy architectures to unified server-side data fetching via asynchronous logic within pages. It covers the correct implementation of search parameter handling, revalidation strategies, and streaming UI components to minimize layout shift while ensuring efficient server-to-client data flow. Users gain a repeatable approach for building scalable directory structures, handling metadata updates, and managing complex application state through Server Actions rather than traditional middleware or separate API routes.

When to Use This Skill

  • β€’Building data-driven e-commerce dashboards with filtering and pagination
  • β€’Migrating existing Pages Router applications to the App Router architecture
  • β€’Constructing complex layouts with shared sidebars and varying content regions
  • β€’Implementing server-side data mutations without exposing database credentials

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • β€œSet up a new Next.js App Router directory structure
  • β€œHow do I move data fetching from a client hook to a server component?
  • β€œGenerate a nested layout with loading and error boundaries
  • β€œRefactor this client-side form to use a Server Action
  • β€œConfigure ISR revalidation for my product detail pages

Pro Tips

  • πŸ’‘Prioritize Server Components for data fetching and heavy logic to minimize client-side bundle size, reserving Client Components strictly for interactive UI elements.
  • πŸ’‘Effectively utilize `revalidatePath` and `revalidateTag` within Server Actions to manage data caching and ensure fresh content, simplifying complex cache invalidation.
  • πŸ’‘Implement `Suspense` boundaries around slow data fetches or complex components to provide instant loading states, significantly improving perceived performance and user experience.

What this skill does

  • β€’Modularizes UI into nested layouts and specialized route segments
  • β€’Integrates Suspense boundaries for granular component streaming
  • β€’Handles search parameter parsing and validation in server-side components
  • β€’Implements Server Actions to replace legacy client-side API calls
  • β€’Configures ISR, static, and dynamic caching policies at the segment level

When not to use it

  • βœ•Developing static, single-page sites that do not require server-side rendering
  • βœ•Projects restricted to older React versions without support for RSC

Example workflow

  1. Initialize app directory structure with layout and page files
  2. Define asynchronous fetching logic directly within the page component
  3. Wrap content in Suspense boundaries for loading states
  4. Create Server Actions for handling form submissions and mutations
  5. Implement search parameter logic to drive filtered UI updates

Prerequisites

  • –Node.js installed
  • –Next.js 14+ project initialized
  • –Basic knowledge of React hooks and async/await syntax

Pitfalls & limitations

  • !Accidentally importing server-only code into a client component without a warning
  • !Over-nesting layouts leading to complex prop drilling or unnecessary re-renders
  • !Neglecting to stringify or manage search parameter objects causing hydration mismatches

FAQ

Why is my component throwing an error when using hooks?
Components are Server Components by default; you must add the 'use client' directive at the top of the file to use hooks like useState or useEffect.
How do I trigger a re-fetch when search params change?
Wrap the component in a Suspense boundary with a unique key tied to the search parameters to force a re-render when the URL updates.
Are Server Actions secure?
Yes, they run on the server and keep logic private, but always validate inputs on the server side to prevent unauthorized access.

How it compares

Unlike manual setup which often leads to inconsistent directory layouts and disorganized fetch logic, this skill enforces a standard architectural pattern that ensures predictable rendering behavior and type safety.

Source & trust

⭐ 37k starsπŸ“„ MITπŸ•’ Updated 2026-06-16
πŸ“„ Full skill instructions β€” original source: wshobson/agents
# 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

// 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 (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
)
}

// 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 (
<main>
<h1>Products</h1>
<ProductGrid products={products} />
</main>
)
}


## Patterns

### Pattern 1: Server Components with Data Fetching

// 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<SearchParams>
}) {
const params = await searchParams

return (
<div className="flex gap-8">
<FilterSidebar />
<Suspense
key={JSON.stringify(params)}
fallback={<ProductListSkeleton />}
>
<ProductList
category={params.category}
sort={params.sort}
page={Number(params.page) || 1}
/>
</Suspense>
</div>
)
}

// 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 (
<div>
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
<Pagination currentPage={page} totalPages={totalPages} />
</div>
)
}


### Pattern 2: Client Components with 'use client'

// 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<string | null>(null)

const handleClick = () => {
setError(null)
startTransition(async () => {
const result = await addToCart(productId)
if (result.error) {
setError(result.error)
}
})
}

return (
<div>
<button
onClick={handleClick}
disabled={isPending}
className="btn-primary"
>
{isPending ? 'Adding...' : 'Add to Cart'}
</button>
{error && <p className="text-red-500 text-sm">{error}</p>}
</div>
)
}


### Pattern 3: Server Actions

// 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

// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div className="dashboard-grid">
<main>{children}</main>
<aside className="analytics-panel">{analytics}</aside>
<aside className="team-panel">{team}</aside>
</div>
)
}

// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
const stats = await getAnalytics()
return <AnalyticsChart data={stats} />
}

// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return <ChartSkeleton />
}

// app/dashboard/@team/page.tsx
export default async function TeamSlot() {
const members = await getTeamMembers()
return <TeamList members={members} />
}


### Pattern 5: Intercepting Routes (Modal Pattern)

// 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 (
<Modal>
<PhotoDetail photo={photo} />
</Modal>
)
}

// 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 (
<div className="photo-page">
<PhotoDetail photo={photo} />
<RelatedPhotos photoId={id} />
</div>
)
}

// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
)
}


### Pattern 6: Streaming with Suspense

// 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 (
<div>
{/* Immediate render */}
<ProductHeader product={product} />

{/* Stream in reviews */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={id} />
</Suspense>

{/* Stream in recommendations */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={id} />
</Suspense>
</div>
)
}

// These components fetch their own data
async function Reviews({ productId }: { productId: string }) {
const reviews = await getReviews(productId) // Slow API
return <ReviewList reviews={reviews} />
}

async function Recommendations({ productId }: { productId: string }) {
const products = await getRecommendations(productId) // ML-based, slow
return <ProductCarousel products={products} />
}


### Pattern 7: Route Handlers (API Routes)

// 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

// 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<Metadata> {
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 <ProductDetail product={product} />
}


## Caching Strategies

### Data Cache

// 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)

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/nextjs-app-router-patterns/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/wshobson/agents/nextjs-app-router-patterns/SKILL.md
  • Cursor: ~/.cursor/skills/wshobson/agents/nextjs-app-router-patterns/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/wshobson/agents/nextjs-app-router-patterns/SKILL.md

πŸš€ Install with CLI:
npx skills add wshobson/agents

Read the Master Guide: Mastering Agent Skills β†’

Recommended Rules

View more rules β†’

Recommended Workflows

View more workflows β†’

Recommended MCP Servers

View more MCP servers β†’

Take It Further

Maximize your productivity with these powerful resources

πŸ“‹

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
πŸ“–

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid next.js issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Next.js and is published by W. Shobson, maintained in wshobson/agents.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.