| name | nextjs |
| description | Use this skill for Next.js App Router patterns, Server Components, Server Actions, Cache Components, and framework-level optimizations. Covers Next.js 16 breaking changes including async params, proxy.ts migration, Cache Components with "use cache", and React 19.2 integration. For deploying to Cloudflare Workers, use the cloudflare-nextjs skill instead. This skill is deployment-agnostic and works with Vercel, AWS, self-hosted, or any platform. |
| license | MIT |
| metadata | [object Object] |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep |
Next.js App Router - Production Patterns
Version: Next.js 16.0.0 React Version: 19.2.0 Node.js: 20.9+ Last Verified: 2025-10-24
Table of Contents
- When to Use This Skill
- When NOT to Use This Skill
- Next.js 16 Breaking Changes
- Cache Components & Caching APIs
- Server Components
- Server Actions
- Route Handlers
- Proxy vs Middleware
- Parallel Routes & Route Groups
- React 19.2 Features
- Metadata API
- Image & Font Optimization
- Performance Patterns
- TypeScript Configuration
- Common Errors & Solutions
- Templates Reference
- Additional Resources
When to Use This Skill
Use this skill when you need:
- Next.js 16 App Router patterns (layouts, loading, error boundaries, routing)
- Server Components best practices (data fetching, composition, streaming)
- Server Actions patterns (forms, mutations, revalidation, error handling)
- Cache Components with
"use cache"directive (NEW in Next.js 16) - New caching APIs:
revalidateTag(),updateTag(),refresh()(Updated in Next.js 16) - Migration from Next.js 15 to 16 (async params, proxy.ts, parallel routes)
- Route Handlers (API endpoints, webhooks, streaming responses)
- Proxy patterns (
proxy.tsreplacesmiddleware.tsin Next.js 16) - Async route params (
params,searchParams,cookies(),headers()now async) - Parallel routes with default.js (breaking change in Next.js 16)
- React 19.2 features (View Transitions,
useEffectEvent(), React Compiler) - Metadata API (SEO, Open Graph, Twitter Cards, sitemaps)
- Image optimization (
next/imagewith updated defaults in Next.js 16) - Font optimization (
next/fontpatterns) - Turbopack configuration (stable and default in Next.js 16)
- Performance optimization (lazy loading, code splitting, PPR, ISR)
- TypeScript configuration (strict mode, path aliases)
When NOT to Use This Skill
Do NOT use this skill for:
- Cloudflare Workers deployment → Use
cloudflare-nextjsskill instead - Pages Router patterns → This skill covers App Router ONLY (Pages Router is legacy)
- Authentication libraries → Use
clerk-auth,auth-js, or other auth-specific skills - Database integration → Use
cloudflare-d1,drizzle-orm-d1, or database-specific skills - UI component libraries → Use
tailwind-v4-shadcnskill for Tailwind + shadcn/ui - State management → Use
zustand-state-management,tanstack-queryskills - Form libraries → Use
react-hook-form-zodskill - Vercel-specific features → Refer to Vercel platform documentation
- Next.js Enterprise features (ISR, DPR) → Refer to Next.js Enterprise docs
- Deployment configuration → Use platform-specific deployment skills
Relationship with Other Skills:
- cloudflare-nextjs: For deploying Next.js to Cloudflare Workers (use BOTH skills together if deploying to Cloudflare)
- tailwind-v4-shadcn: For Tailwind v4 + shadcn/ui setup (composable with this skill)
- clerk-auth: For Clerk authentication in Next.js (composable with this skill)
- auth-js: For Auth.js (NextAuth) integration (composable with this skill)
Next.js 16 Breaking Changes
IMPORTANT: Next.js 16 introduces multiple breaking changes. Read this section carefully if migrating from Next.js 15 or earlier.
1. Async Route Parameters (BREAKING)
Breaking Change: params, searchParams, cookies(), headers(), draftMode() are now async and must be awaited.
Before (Next.js 15):
// ❌ This no longer works in Next.js 16
export default function Page({ params, searchParams }: {
params: { slug: string }
searchParams: { query: string }
}) {
const slug = params.slug // ❌ Error: params is a Promise
const query = searchParams.query // ❌ Error: searchParams is a Promise
return <div>{slug}</div>
}
After (Next.js 16):
// ✅ Correct: await params and searchParams
export default async function Page({ params, searchParams }: {
params: Promise<{ slug: string }>
searchParams: Promise<{ query: string }>
}) {
const { slug } = await params // ✅ Await the promise
const { query } = await searchParams // ✅ Await the promise
return <div>{slug}</div>
}
Applies to:
paramsin pages, layouts, route handlerssearchParamsin pagescookies()fromnext/headersheaders()fromnext/headersdraftMode()fromnext/headers
Migration:
// ❌ Before
import { cookies, headers } from 'next/headers'
export function MyComponent() {
const cookieStore = cookies() // ❌ Sync access
const headersList = headers() // ❌ Sync access
}
// ✅ After
import { cookies, headers } from 'next/headers'
export async function MyComponent() {
const cookieStore = await cookies() // ✅ Async access
const headersList = await headers() // ✅ Async access
}
Codemod: Run npx @next/codemod@canary upgrade latest to automatically migrate.
See Template: templates/app-router-async-params.tsx
2. Middleware → Proxy Migration (BREAKING)
Breaking Change: middleware.ts is deprecated in Next.js 16. Use proxy.ts instead.
Why the Change: proxy.ts makes the network boundary explicit by running on Node.js runtime (not Edge runtime). This provides better clarity between edge middleware and server-side proxies.
Migration Steps:
- Rename file:
middleware.ts→proxy.ts - Rename function:
middleware→proxy - Update config:
matcher→config.matcher(same syntax)
Before (Next.js 15):
// middleware.ts ❌ Deprecated in Next.js 16
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: '/api/:path*',
}
After (Next.js 16):
// proxy.ts ✅ New in Next.js 16
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: '/api/:path*',
}
Note: middleware.ts still works in Next.js 16 but is deprecated. Migrate to proxy.ts for future compatibility.
See Template: templates/proxy-migration.ts
See Reference: references/proxy-vs-middleware.md
3. Parallel Routes Require default.js (BREAKING)
Breaking Change: Parallel routes now require explicit default.js files. Without them, routes will fail during soft navigation.
Structure:
app/
├── @auth/
│ ├── login/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
├── @dashboard/
│ ├── overview/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
└── layout.tsx
Layout:
// app/layout.tsx
export default function Layout({
children,
auth,
dashboard,
}: {
children: React.ReactNode
auth: React.ReactNode
dashboard: React.ReactNode
}) {
return (
<html>
<body>
{auth}
{dashboard}
{children}
</body>
</html>
)
}
Default Fallback (REQUIRED):
// app/@auth/default.tsx
export default function AuthDefault() {
return null // or <Skeleton /> or redirect
}
// app/@dashboard/default.tsx
export default function DashboardDefault() {
return null
}
Why Required: Next.js 16 changed how parallel routes handle soft navigation. Without default.js, unmatched slots will error during client-side navigation.
See Template: templates/parallel-routes-with-default.tsx
4. Removed Features (BREAKING)
The following features are REMOVED in Next.js 16:
- AMP Support - Entirely removed. Migrate to standard pages.
next lintcommand - Use ESLint or Biome directly.serverRuntimeConfigandpublicRuntimeConfig- Use environment variables instead.experimental.pprflag - Evolved into Cache Components. Use"use cache"directive.- Automatic
scroll-behavior: smooth- Add manually if needed. - Node.js 18 support - Minimum version is now 20.9+.
Migration:
- AMP: Convert AMP pages to standard pages or use separate AMP implementation.
- Linting: Run
npx eslint .ornpx biome lint .directly. - Config: Replace
serverRuntimeConfigwithprocess.env.VARIABLE. - PPR: Migrate from
experimental.pprto"use cache"directive (see Cache Components section).
5. Version Requirements (BREAKING)
Next.js 16 requires:
- Node.js: 20.9+ (Node.js 18 no longer supported)
- TypeScript: 5.1+ (if using TypeScript)
- React: 19.2+ (automatically installed with Next.js 16)
- Browsers: Chrome 111+, Safari 16.4+, Firefox 109+, Edge 111+
Check Versions:
node --version # Should be 20.9+
npm --version # Should be 10+
npx next --version # Should be 16.0.0+
Upgrade Node.js:
# Using nvm
nvm install 20
nvm use 20
nvm alias default 20
# Using Homebrew (macOS)
brew install node@20
# Using apt (Ubuntu/Debian)
sudo apt update
sudo apt install nodejs npm
6. Image Defaults Changed (BREAKING)
Next.js 16 changed next/image defaults:
| Setting | Next.js 15 | Next.js 16 |
|---|---|---|
| TTL (cache duration) | 60 seconds | 4 hours |
| imageSizes | [16, 32, 48, 64, 96, 128, 256, 384] |
[640, 750, 828, 1080, 1200] (reduced) |
| qualities | [75, 90, 100] |
[75] (single quality) |
Impact:
- Images cache longer (4 hours vs 60 seconds)
- Fewer image sizes generated (smaller builds, but less granular)
- Single quality (75) generated instead of multiple
Override Defaults (if needed):
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
minimumCacheTTL: 60, // Revert to 60 seconds
deviceSizes: [640, 750, 828, 1080, 1200, 1920], // Add larger sizes
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Restore old sizes
formats: ['image/webp'], // Default
},
}
export default config
See Template: templates/image-optimization.tsx
Cache Components & Caching APIs
NEW in Next.js 16: Cache Components introduce opt-in caching with the "use cache" directive, replacing implicit caching from Next.js 15.
1. Overview
What Changed:
- Next.js 15: Implicit caching (all Server Components cached by default)
- Next.js 16: Opt-in caching with
"use cache"directive
Why the Change: Explicit caching gives developers more control and makes caching behavior predictable.
Cache Components enable:
- Component-level caching (cache specific components, not entire pages)
- Function-level caching (cache expensive computations)
- Page-level caching (cache entire pages selectively)
- Partial Prerendering (PPR) - Cache static parts, render dynamic parts on-demand
2. "use cache" Directive
Syntax: Add "use cache" at the top of a Server Component, function, or route handler.
Component-level caching:
// app/components/expensive-component.tsx
'use cache'
export async function ExpensiveComponent() {
const data = await fetch('https://api.example.com/data')
const json = await data.json()
return (
<div>
<h1>{json.title}</h1>
<p>{json.description}</p>
</div>
)
}
Function-level caching:
// lib/data.ts
'use cache'
export async function getExpensiveData(id: string) {
const response = await fetch(`https://api.example.com/items/${id}`)
return response.json()
}
// Usage in component
import { getExpensiveData } from '@/lib/data'
export async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const product = await getExpensiveData(id) // Cached
return <div>{product.name}</div>
}
Page-level caching:
// app/blog/[slug]/page.tsx
'use cache'
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
See Template: templates/cache-component-use-cache.tsx
3. Partial Prerendering (PPR)
PPR allows caching static parts of a page while rendering dynamic parts on-demand.
Pattern:
// app/dashboard/page.tsx
// Static header (cached)
'use cache'
async function StaticHeader() {
return <header>My App</header>
}
// Dynamic user info (not cached)
async function DynamicUserInfo() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const user = await fetch(`/api/users/${userId}`).then(r => r.json())
return <div>Welcome, {user.name}</div>
}
// Page combines both
export default function Dashboard() {
return (
<div>
<StaticHeader /> {/* Cached */}
<DynamicUserInfo /> {/* Dynamic */}
</div>
)
}
When to Use PPR:
- Page has both static and dynamic content
- Want to cache layout/header/footer but render user-specific content
- Need fast initial load (static parts) + personalization (dynamic parts)
See Reference: references/cache-components-guide.md
4. revalidateTag() - Updated API
BREAKING CHANGE: revalidateTag() now requires a second argument (cacheLife profile) for stale-while-revalidate behavior.
Before (Next.js 15):
import { revalidateTag } from 'next/cache'
export async function updatePost(id: string) {
await fetch(`/api/posts/${id}`, { method: 'PATCH' })
revalidateTag('posts') // ❌ Only one argument in Next.js 15
}
After (Next.js 16):
import { revalidateTag } from 'next/cache'
export async function updatePost(id: string) {
await fetch(`/api/posts/${id}`, { method: 'PATCH' })
revalidateTag('posts', 'max') // ✅ Second argument required in Next.js 16
}
Built-in Cache Life Profiles:
'max'- Maximum staleness (recommended for most use cases)'hours'- Stale after hours'days'- Stale after days'weeks'- Stale after weeks'default'- Default cache behavior
Custom Cache Life Profile:
revalidateTag('posts', {
stale: 3600, // Stale after 1 hour (seconds)
revalidate: 86400, // Revalidate every 24 hours (seconds)
expire: false, // Never expire (optional)
})
Pattern in Server Actions:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title, content }),
})
revalidateTag('posts', 'max') // ✅ Revalidate with max staleness
}
See Template: templates/revalidate-tag-cache-life.ts
5. updateTag() - NEW API (Server Actions Only)
NEW in Next.js 16: updateTag() provides read-your-writes semantics for Server Actions.
What it does:
- Expires cache immediately
- Refreshes data within the same request
- Shows updated data right after mutation (no stale data)
Difference from revalidateTag():
revalidateTag(): Stale-while-revalidate (shows stale data, revalidates in background)updateTag(): Immediate refresh (expires cache, fetches fresh data in same request)
Use Case: Forms, user settings, or any mutation where user expects immediate feedback.
Pattern:
'use server'
import { updateTag } from 'next/cache'
export async function updateUserProfile(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
// Update database
await db.users.update({ name, email })
// Immediately refresh cache (read-your-writes)
updateTag('user-profile')
// User sees updated data immediately (no stale data)
}
When to Use:
updateTag(): User settings, profile updates, critical mutations (immediate feedback)revalidateTag(): Blog posts, product listings, non-critical updates (background revalidation)
See Template: templates/server-action-update-tag.ts
6. refresh() - NEW API (Server Actions Only)
NEW in Next.js 16: refresh() refreshes uncached data only (complements client-side router.refresh()).
When to Use:
- Refresh dynamic data without affecting cached data
- Complement
router.refresh()on server side
Pattern:
'use server'
import { refresh } from 'next/cache'
export async function refreshDashboard() {
// Refresh uncached data (e.g., real-time metrics)
refresh()
// Cached data (e.g., static header) remains cached
}
Difference from revalidateTag() and updateTag():
refresh(): Only refreshes uncached datarevalidateTag(): Revalidates specific tagged data (stale-while-revalidate)updateTag(): Immediately expires and refreshes specific tagged data
See Reference: references/cache-components-guide.md
Server Components
Server Components are React components that render on the server. They enable efficient data fetching, reduce client bundle size, and improve performance.
1. Server Component Basics
Default Behavior: All components in the App Router are Server Components by default (unless marked with 'use client').
Example:
// app/posts/page.tsx (Server Component by default)
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<div>
{posts.map((post: { id: string; title: string }) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
Rules:
- ✅ Can
awaitpromises directly in component body - ✅ Can access
cookies(),headers(),draftMode()(withawait) - ✅ Can use Node.js APIs (fs, path, etc.)
- ❌ Cannot use browser APIs (window, document, localStorage)
- ❌ Cannot use React hooks (
useState,useEffect, etc.) - ❌ Cannot use event handlers (
onClick,onChange, etc.)
2. Data Fetching in Server Components
Pattern: Use async/await directly in component body.
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
// Fetch data directly in component
const product = await fetch(`https://api.example.com/products/${id}`)
.then(r => r.json())
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
</div>
)
}
Parallel Data Fetching:
export default async function Dashboard() {
// Fetch in parallel with Promise.all
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
])
return (
<div>
<UserInfo user={user} />
<PostsList posts={posts} />
<CommentsList comments={comments} />
</div>
)
}
Sequential Data Fetching (when needed):
export default async function UserPosts({ params }: { params: Promise<{ userId: string }> }) {
const { userId } = await params
// Fetch user first
const user = await fetch(`/api/users/${userId}`).then(r => r.json())
// Then fetch user's posts (depends on user data)
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json())
return (
<div>
<h1>{user.name}'s Posts</h1>
<ul>
{posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
See Template: templates/server-component-streaming.tsx
3. Streaming with Suspense
Pattern: Wrap slow components in <Suspense> to stream content as it loads.
import { Suspense } from 'react'
// Fast component (loads immediately)
async function Header() {
return <header>My App</header>
}
// Slow component (takes 2 seconds)
async function SlowData() {
await new Promise(resolve => setTimeout(resolve, 2000))
const data = await fetch('/api/slow-data').then(r => r.json())
return <div>{data.content}</div>
}
// Page streams content
export default function Page() {
return (
<div>
<Header /> {/* Loads immediately */}
<Suspense fallback={<div>Loading...</div>}>
<SlowData /> {/* Streams when ready */}
</Suspense>
</div>
)
}
When to Use Streaming:
- Page has slow API calls
- Want to show UI immediately (don't wait for all data)
- Improve perceived performance
See Reference: references/server-components-patterns.md
4. Server vs Client Components
When to Use Server Components (default):
- Fetch data from APIs/databases
- Access backend resources (files, environment variables)
- Keep large dependencies on server (reduce bundle size)
- Render static content
When to Use Client Components ('use client'):
- Need React hooks (
useState,useEffect,useContext) - Need event handlers (
onClick,onChange,onSubmit) - Need browser APIs (
window,localStorage,navigator) - Need third-party libraries that use browser APIs (charts, maps, etc.)
Pattern: Use Server Components by default, add 'use client' only when needed.
// app/components/interactive-button.tsx
'use client' // Client Component
import { useState } from 'react'
export function InteractiveButton() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
)
}
// app/page.tsx (Server Component)
import { InteractiveButton } from './components/interactive-button'
export default async function Page() {
const data = await fetch('/api/data').then(r => r.json())
return (
<div>
<h1>{data.title}</h1>
<InteractiveButton /> {/* Client Component inside Server Component */}
</div>
)
}
Composition Rules:
- ✅ Server Component can import Client Component
- ✅ Client Component can import Client Component
- ✅ Client Component can render Server Component as children (via props)
- ❌ Client Component cannot import Server Component directly
See Reference: references/server-components-patterns.md
Server Actions
Server Actions are asynchronous functions that run on the server. They enable server-side mutations, form handling, and data revalidation.
1. Server Action Basics
Definition: Add 'use server' directive to create a Server Action.
File-level Server Actions:
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// Mutate database
await db.posts.create({ title, content })
// Revalidate cache
revalidateTag('posts', 'max')
}
Inline Server Actions:
// app/posts/new/page.tsx
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.posts.create({ title, content })
revalidateTag('posts', 'max')
}
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<button type="submit">Create Post</button>
</form>
)
}
See Template: templates/server-actions-form.tsx
2. Form Handling
Basic Form:
// app/components/create-post-form.tsx
import { createPost } from '@/app/actions'
export function CreatePostForm() {
return (
<form action={createPost}>
<label>
Title:
<input type="text" name="title" required />
</label>
<label>
Content:
<textarea name="content" required />
</label>
<button type="submit">Create Post</button>
</form>
)
}
With Loading State (useFormStatus):
'use client'
import { useFormStatus } from 'react-dom'
import { createPost } from '@/app/actions'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
export function CreatePostForm() {
return (
<form action={createPost}>
<input type="text" name="title" required />
<textarea name="content" required />
<SubmitButton />
</form>
)
}
With Validation:
// app/actions.ts
'use server'
import { z } from 'zod'
import { redirect } from 'next/navigation'
const PostSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters'),
content: z.string().min(10, 'Content must be at least 10 characters'),
})
export async function createPost(formData: FormData) {
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
}
// Validate
const parsed = PostSchema.safeParse(rawData)
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors,
}
}
// Mutate
await db.posts.create(parsed.data)
// Revalidate and redirect
revalidateTag('posts', 'max')
redirect('/posts')
}
See Template: templates/server-actions-form.tsx
See Reference: references/server-actions-guide.md
3. Error Handling
Pattern: Return error objects from Server Actions, handle in Client Components.
Server Action:
// app/actions.ts
'use server'
export async function deletePost(id: string) {
try {
await db.posts.delete({ where: { id } })
revalidateTag('posts', 'max')
return { success: true }
} catch (error) {
return {
success: false,
error: 'Failed to delete post. Please try again.'
}
}
}
Client Component:
'use client'
import { useState } from 'react'
import { deletePost } from '@/app/actions'
export function DeleteButton({ postId }: { postId: string }) {
const [error, setError] = useState<string | null>(null)
async function handleDelete() {
const result = await deletePost(postId)
if (!result.success) {
setError(result.error)
}
}
return (
<div>
<button onClick={handleDelete}>Delete Post</button>
{error && <p className="error">{error}</p>}
</div>
)
}
4. Optimistic Updates
Pattern: Show UI update immediately, then sync with server.
'use client'
import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, amount: number) => state + amount
)
async function handleLike() {
// Update UI immediately
addOptimisticLike(1)
// Sync with server
await likePost(postId)
}
return (
<button onClick={handleLike}>
❤️ {optimisticLikes} likes
</button>
)
}
See Reference: references/server-actions-guide.md
Route Handlers
Route Handlers are server-side API endpoints in the App Router. They replace API Routes from the Pages Router.
1. Basic Route Handler
File: app/api/hello/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ message: 'Hello, World!' })
}
export async function POST(request: Request) {
const body = await request.json()
return NextResponse.json({
message: 'Post created',
data: body
})
}
Supported Methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
See Template: templates/route-handler-api.ts
2. Dynamic Routes
File: app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params // ✅ Await params in Next.js 16
const post = await db.posts.findUnique({ where: { id } })
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
await db.posts.delete({ where: { id } })
return NextResponse.json({ message: 'Post deleted' })
}
3. Search Params
URL: /api/posts?tag=javascript&limit=10
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const tag = searchParams.get('tag')
const limit = parseInt(searchParams.get('limit') || '10')
const posts = await db.posts.findMany({
where: { tags: { has: tag } },
take: limit,
})
return NextResponse.json(posts)
}
4. Webhooks
Pattern: Handle incoming webhooks from third-party services.
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
export async function POST(request: Request) {
const body = await request.text()
const headersList = await headers() // ✅ Await headers in Next.js 16
const signature = headersList.get('stripe-signature')
// Verify webhook signature
const event = stripe.webhooks.constructEvent(
body,
signature!,
process.env.STRIPE_WEBHOOK_SECRET!
)
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object)
break
case 'payment_intent.failed':
await handlePaymentFailure(event.data.object)
break
}
return NextResponse.json({ received: true })
}
See Template: templates/route-handler-api.ts
See Reference: references/route-handlers-reference.md
Proxy vs Middleware
Next.js 16 introduces proxy.ts to replace middleware.ts.
Why the Change?
middleware.ts: Runs on Edge runtime (limited Node.js APIs)proxy.ts: Runs on Node.js runtime (full Node.js APIs)
The new proxy.ts makes the network boundary explicit and provides more flexibility.
Migration
Before (middleware.ts):
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check auth
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}
After (proxy.ts):
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
// Check auth
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}
See Template: templates/proxy-migration.ts
See Reference: references/proxy-vs-middleware.md
Parallel Routes & Route Groups
1. Parallel Routes
Use Case: Render multiple pages in the same layout (e.g., modal + main content, dashboard panels).
Structure:
app/
├── @modal/
│ ├── login/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
├── @feed/
│ ├── trending/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
└── layout.tsx
Layout:
// app/layout.tsx
export default function Layout({
children,
modal,
feed,
}: {
children: React.ReactNode
modal: React.ReactNode
feed: React.ReactNode
}) {
return (
<html>
<body>
{modal}
<main>
{children}
<aside>{feed}</aside>
</main>
</body>
</html>
)
}
Default Files (REQUIRED):
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}
// app/@feed/default.tsx
export default function FeedDefault() {
return <div>Default Feed</div>
}
See Template: templates/parallel-routes-with-default.tsx
2. Route Groups
Use Case: Organize routes without affecting URL structure.
Structure:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx → /about
│ └── contact/
│ └── page.tsx → /contact
├── (shop)/
│ ├── products/
│ │ └── page.tsx → /products
│ └── cart/
│ └── page.tsx → /cart
└── layout.tsx
Different Layouts per Group:
app/
├── (marketing)/
│ ├── layout.tsx ← Marketing layout
│ └── about/page.tsx
├── (shop)/
│ ├── layout.tsx ← Shop layout
│ └── products/page.tsx
└── layout.tsx ← Root layout
See Reference: references/app-router-fundamentals.md
React 19.2 Features
Next.js 16 integrates React 19.2, which includes new features from React Canary.
1. View Transitions
Use Case: Smooth animations between page transitions.
'use client'
import { useRouter } from 'next/navigation'
import { startTransition } from 'react'
export function NavigationLink({ href, children }: { href: string; children: React.ReactNode }) {
const router = useRouter()
function handleClick(e: React.MouseEvent) {
e.preventDefault()
// Wrap navigation in startTransition for View Transitions
startTransition(() => {
router.push(href)
})
}
return <a href={href} onClick={handleClick}>{children}</a>
}
With CSS View Transitions API:
/* app/globals.css */
@view-transition {
navigation: auto;
}
/* Animate elements with view-transition-name */
.page-title {
view-transition-name: page-title;
}
See Template: templates/view-transitions-react-19.tsx
2. useEffectEvent() (Experimental)
Use Case: Extract non-reactive logic from useEffect.
'use client'
import { useEffect, experimental_useEffectEvent as useEffectEvent } from 'react'
export function ChatRoom({ roomId }: { roomId: string }) {
const onConnected = useEffectEvent(() => {
console.log('Connected to room:', roomId)
})
useEffect(() => {
const connection = connectToRoom(roomId)
onConnected() // Non-reactive callback
return () => connection.disconnect()
}, [roomId]) // Only re-run when roomId changes
return <div>Chat Room {roomId}</div>
}
Why Use It: Prevents unnecessary useEffect re-runs when callback dependencies change.
3. React Compiler (Stable)
Use Case: Automatic memoization without useMemo, useCallback.
Enable in next.config.ts:
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
reactCompiler: true,
},
}
export default config
Install Plugin:
npm install babel-plugin-react-compiler
Example (no manual memoization needed):
'use client'
export function ExpensiveList({ items }: { items: string[] }) {
// React Compiler automatically memoizes this
const filteredItems = items.filter(item => item.length > 3)
return (
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
)
}
See Reference: references/react-19-integration.md
Metadata API
The Metadata API provides type-safe SEO and social sharing metadata.
1. Static Metadata
Pattern: Export metadata object from page or layout.
// app/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My App',
description: 'Welcome to my app',
openGraph: {
title: 'My App',
description: 'Welcome to my app',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'My App',
description: 'Welcome to my app',
images: ['/twitter-image.jpg'],
},
}
export default function Page() {
return <h1>Home</h1>
}
2. Dynamic Metadata
Pattern: Export generateMetadata async function.
// app/posts/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
return <article>{post.content}</article>
}
3. Sitemap
File: app/sitemap.ts
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetch('/api/posts').then(r => r.json())
const postUrls = posts.map((post: { id: string; updatedAt: string }) => ({
url: `https://example.com/posts/${post.id}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}))
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...postUrls,
]
}
See Template: templates/metadata-config.ts
See Reference: references/metadata-api-guide.md
Image & Font Optimization
1. next/image
Basic Usage:
import Image from 'next/image'
export function MyImage() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Load above the fold
/>
)
}
Responsive Images:
<Image
src="/hero.jpg"
alt="Hero"
fill
style={{ objectFit: 'cover' }}
sizes="(max-width: 768px) 100vw, 50vw"
/>
Remote Images (configure in next.config.ts):
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
],
},
}
export default config
See Template: templates/image-optimization.tsx
2. next/font
Google Fonts:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}
Local Fonts:
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/my-font.woff2',
display: 'swap',
variable: '--font-my-font',
})
See Template: templates/font-optimization.tsx
Performance Patterns
1. Lazy Loading
Component Lazy Loading:
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./heavy-component'), {
loading: () => <div>Loading...</div>,
ssr: false, // Disable SSR for client-only components
})
export default function Page() {
return (
<div>
<h1>My Page</h1>
<HeavyComponent />
</div>
)
}
Conditional Loading:
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./chart'), { ssr: false })
export function Dashboard() {
const [showChart, setShowChart] = useState(false)
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && <Chart />}
</div>
)
}
2. Code Splitting
Route-level Splitting (automatic):
app/
├── page.tsx → Bundles: /, shared
├── about/
│ └── page.tsx → Bundles: /about, shared
└── products/
└── page.tsx → Bundles: /products, shared
Component-level Splitting (with dynamic import):
const Analytics = dynamic(() => import('./analytics'))
const Comments = dynamic(() => import('./comments'))
export default function BlogPost() {
return (
<article>
<h1>Post Title</h1>
<p>Content...</p>
<Analytics /> {/* Separate bundle */}
<Comments /> {/* Separate bundle */}
</article>
)
}
3. Turbopack (Stable in Next.js 16)
Default: Turbopack is now the default bundler in Next.js 16.
Metrics:
- 2–5× faster production builds
- Up to 10× faster Fast Refresh
Opt-out (if needed):
npm run build -- --webpack
Enable File System Caching (beta):
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
turbopack: {
fileSystemCaching: true, // Beta: Persist cache between runs
},
},
}
export default config
See Reference: references/performance-optimization.md
TypeScript Configuration
1. Strict Mode
Enable strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
2. Path Aliases
Configure in tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./app/*"],
"@/components/*": ["./app/components/*"],
"@/lib/*": ["./lib/*"],
"@/styles/*": ["./styles/*"]
}
}
}
Usage:
// Instead of: import { Button } from '../../../components/button'
import { Button } from '@/components/button'
3. Type-Safe Routing
Generate types from routes:
npm run build
Usage:
import { useRouter } from 'next/navigation'
const router = useRouter()
// Type-safe routing
router.push('/posts/123') // ✅ Valid route
router.push('/invalid') // ❌ Type error if route doesn't exist
See Reference: references/typescript-configuration.md
Common Errors & Solutions
1. Error: params is a Promise
Error:
Type 'Promise<{ id: string }>' is not assignable to type '{ id: string }'
Cause: Next.js 16 changed params to async.
Solution: Await params:
// ❌ Before
export default function Page({ params }: { params: { id: string } }) {
const id = params.id
}
// ✅ After
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
}
2. Error: searchParams is a Promise
Error:
Property 'query' does not exist on type 'Promise<{ query: string }>'
Cause: searchParams is now async in Next.js 16.
Solution:
// ❌ Before
export default function Page({ searchParams }: { searchParams: { query: string } }) {
const query = searchParams.query
}
// ✅ After
export default async function Page({ searchParams }: { searchParams: Promise<{ query: string }> }) {
const { query } = await searchParams
}
3. Error: cookies() requires await
Error:
'cookies' implicitly has return type 'any'
Cause: cookies() is now async in Next.js 16.
Solution:
// ❌ Before
import { cookies } from 'next/headers'
export function MyComponent() {
const cookieStore = cookies()
}
// ✅ After
import { cookies } from 'next/headers'
export async function MyComponent() {
const cookieStore = await cookies()
}
4. Error: Parallel route missing default.js
Error:
Error: Parallel route @modal/login was matched but no default.js was found
Cause: Next.js 16 requires default.js for all parallel routes.
Solution: Add default.tsx files:
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}
5. Error: revalidateTag() requires 2 arguments
Error:
Expected 2 arguments, but got 1
Cause: revalidateTag() now requires a cacheLife argument in Next.js 16.
Solution:
// ❌ Before
revalidateTag('posts')
// ✅ After
revalidateTag('posts', 'max')
6. Error: Cannot use React hooks in Server Component
Error:
You're importing a component that needs useState. It only works in a Client Component
Cause: Using React hooks in Server Component.
Solution: Add 'use client' directive:
// ✅ Add 'use client' at the top
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
7. Error: middleware.ts is deprecated
Warning:
Warning: middleware.ts is deprecated. Use proxy.ts instead.
Solution: Migrate to proxy.ts:
// Rename: middleware.ts → proxy.ts
// Rename function: middleware → proxy
export function proxy(request: NextRequest) {
// Same logic
}
8. Error: Turbopack build failure
Error:
Error: Failed to compile with Turbopack
Cause: Turbopack is now default in Next.js 16.
Solution: Opt out of Turbopack if incompatible:
npm run build -- --webpack
9. Error: Invalid next/image src
Error:
Invalid src prop (https://example.com/image.jpg) on `next/image`. Hostname "example.com" is not configured under images in your `next.config.js`
Solution: Add remote patterns in next.config.ts:
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
},
}
10. Error: Cannot import Server Component into Client Component
Error:
You're importing a Server Component into a Client Component
Solution: Pass Server Component as children:
// ❌ Wrong
'use client'
import { ServerComponent } from './server-component' // Error
export function ClientComponent() {
return <ServerComponent />
}
// ✅ Correct
'use client'
export function ClientComponent({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
// Usage
<ClientComponent>
<ServerComponent /> {/* Pass as children */}
</ClientComponent>
11. Error: generateStaticParams not working
Cause: generateStaticParams only works with static generation (export const dynamic = 'force-static').
Solution:
export const dynamic = 'force-static'
export async function generateStaticParams() {
const posts = await fetch('/api/posts').then(r => r.json())
return posts.map((post: { id: string }) => ({ id: post.id }))
}
12. Error: fetch() not caching
Cause: Next.js 16 uses opt-in caching with "use cache" directive.
Solution: Add "use cache" to component or function:
'use cache'
export async function getPosts() {
const response = await fetch('/api/posts')
return response.json()
}
13. Error: Route collision with Route Groups
Error:
Error: Conflicting routes: /about and /(marketing)/about
Cause: Route groups create same URL path.
Solution: Ensure route groups don't conflict:
app/
├── (marketing)/about/page.tsx → /about
└── (shop)/about/page.tsx → ERROR: Duplicate /about
# Fix: Use different routes
app/
├── (marketing)/about/page.tsx → /about
└── (shop)/store-info/page.tsx → /store-info
14. Error: Metadata not updating
Cause: Using dynamic metadata without generateMetadata().
Solution: Use generateMetadata() for dynamic pages:
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
return {
title: post.title,
description: post.excerpt,
}
}
15. Error: next/font font not loading
Cause: Font variable not applied to HTML element.
Solution: Apply font variable to <html> or <body>:
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html className={inter.variable}> {/* ✅ Apply variable */}
<body>{children}</body>
</html>
)
}
16. Error: Environment variables not available in browser
Cause: Server-only env vars are not exposed to browser.
Solution: Prefix with NEXT_PUBLIC_ for client-side access:
# .env
SECRET_KEY=abc123 # Server-only
NEXT_PUBLIC_API_URL=https://api # Available in browser
// Server Component (both work)
const secret = process.env.SECRET_KEY
const apiUrl = process.env.NEXT_PUBLIC_API_URL
// Client Component (only public vars work)
const apiUrl = process.env.NEXT_PUBLIC_API_URL
17. Error: Server Action not found
Error:
Error: Could not find Server Action
Cause: Missing 'use server' directive.
Solution: Add 'use server':
// ❌ Before
export async function createPost(formData: FormData) {
await db.posts.create({ ... })
}
// ✅ After
'use server'
export async function createPost(formData: FormData) {
await db.posts.create({ ... })
}
18. Error: TypeScript path alias not working
Cause: Incorrect baseUrl or paths in tsconfig.json.
Solution: Configure correctly:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@/components/*": ["./app/components/*"]
}
}
}
See Reference: references/top-errors.md
Templates Reference
The following templates are available in templates/:
App Router Fundamentals:
app-router-async-params.tsx- Async params, searchParams patterns (Next.js 16)parallel-routes-with-default.tsx- Parallel routes with required default.jsroute-groups-example.tsx- Route groups organization
Cache Components (Next.js 16):
cache-component-use-cache.tsx- Cache Components with"use cache"partial-prerendering.tsx- PPR with static + dynamic partsrevalidate-tag-cache-life.ts- UpdatedrevalidateTag()APIserver-action-update-tag.ts-updateTag()for read-your-writes
Server Components:
server-component-data-fetching.tsx- Data fetching patternsserver-component-streaming.tsx- Streaming with Suspenseserver-component-composition.tsx- Server + Client component composition
Server Actions:
server-actions-form.tsx- Form handling with Server Actionsserver-actions-validation.ts- Server Action validation with Zodserver-actions-optimistic.tsx- Optimistic updates
Route Handlers:
route-handler-api.ts- Basic CRUD APIroute-handler-webhook.ts- Webhook handlingroute-handler-streaming.ts- Streaming responses
Proxy & Middleware:
proxy-migration.ts- Migrate from middleware.ts to proxy.tsproxy-auth.ts- Auth with proxy.ts
React 19.2:
view-transitions-react-19.tsx- View Transitions APIuse-effect-event.tsx-useEffectEvent()patternreact-compiler-example.tsx- React Compiler usage
Metadata:
metadata-config.ts- Static and dynamic metadatasitemap.ts- Sitemap generationrobots.ts- robots.txt generation
Optimization:
image-optimization.tsx- next/image patternsfont-optimization.tsx- next/font patternslazy-loading.tsx- Dynamic imports and lazy loadingcode-splitting.tsx- Code splitting strategies
TypeScript:
typescript-config.json- Recommended TypeScript configurationpath-aliases.tsx- Path alias usage
Configuration:
next.config.ts- Full Next.js configurationpackage.json- Recommended dependencies for Next.js 16
Additional Resources
Bundled References (in references/):
next-16-migration-guide.md- Complete migration from Next.js 15 to 16cache-components-guide.md- Cache Components deep diveproxy-vs-middleware.md- Proxy.ts vs middleware.ts comparisonasync-route-params.md- Async params, searchParams, cookies(), headers()app-router-fundamentals.md- App Router concepts and patternsserver-components-patterns.md- Server Components best practicesserver-actions-guide.md- Server Actions patterns and validationroute-handlers-reference.md- Route Handlers API referencemetadata-api-guide.md- Metadata API complete guideperformance-optimization.md- Performance patterns and Turbopackreact-19-integration.md- React 19.2 features in Next.jstop-errors.md- 18+ common errors and solutions
Scripts:
scripts/check-versions.sh- Verify Next.js and dependency versions
External Documentation:
- Next.js 16 Blog: https://nextjs.org/blog/next-16
- Next.js Docs: https://nextjs.org/docs
- React Docs: https://react.dev
- Context7 MCP: Use
/websites/nextjsfor latest Next.js reference
Version Compatibility
| Package | Minimum Version | Recommended |
|---|---|---|
| Next.js | 16.0.0 | 16.0.0+ |
| React | 19.2.0 | 19.2.0+ |
| Node.js | 20.9.0 | 20.9.0+ |
| TypeScript | 5.1.0 | 5.7.0+ |
| Turbopack | (built-in) | Stable |
Check Versions:
./scripts/check-versions.sh
Token Efficiency
Estimated Token Savings: 65-70%
Without Skill (manual setup from docs):
- Read Next.js 16 migration guide: ~5k tokens
- Read App Router docs: ~8k tokens
- Read Server Actions docs: ~4k tokens
- Read Metadata API docs: ~3k tokens
- Trial-and-error fixes: ~8k tokens
- Total: ~28k tokens
With Skill:
- Load skill: ~8k tokens
- Use templates: ~2k tokens
- Total: ~10k tokens
- Savings:
18k tokens (64%)
Errors Prevented: 18+ common mistakes = 100% error prevention
Maintenance
Last Verified: 2025-10-24 Next Review: 2026-01-24 (Quarterly) Maintainer: Jezweb | jeremy@jezweb.net Repository: https://github.com/jezweb/claude-skills
Update Triggers:
- Next.js major/minor releases
- React major releases
- Breaking changes in APIs
- New Turbopack features
Version Check:
cd skills/nextjs
./scripts/check-versions.sh
End of SKILL.md