
Nextjs
- 63 installs
- 51 repo stars
- Updated November 25, 2025
- ovachiever/droid-tings
Helps with ai & agent building tasks during AI-assisted development.
About
nextjs is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- nextjs
- AI & Agent Building
- AI-coding skill
Nextjs by the numbers
- 63 all-time installs (skills.sh)
- Ranked #6,083 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ovachiever/droid-tings --skill nextjsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 63 |
|---|---|
| repo stars | ★ 51 |
| Last updated | November 25, 2025 |
| Repository | ovachiever/droid-tings ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
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
1. When to Use This Skill 2. When NOT to Use This Skill 3. Next.js 16 Breaking Changes 4. Cache Components & Caching APIs 5. Route Handlers (Next.js 16 Updates) 6. Proxy vs Middleware 7. Parallel Routes - default.js Required 8. React 19.2 Features 9. Turbopack (Stable in Next.js 16) 10. Common Errors & Solutions 11. Templates & Resources
---
When to Use This Skill
Focus: Next.js 16 breaking changes and knowledge gaps (December 2024+).
Use this skill when you need:
- Next.js 16 breaking changes (async params, proxy.ts, parallel routes default.js, removed features)
- 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 (avoid breaking change errors)
- Async route params (
params,searchParams,cookies(),headers()now async) - Parallel routes with default.js (REQUIRED in Next.js 16)
- React 19.2 features (View Transitions,
useEffectEvent(), React Compiler) - Turbopack (stable and default in Next.js 16)
- Image defaults changed (TTL, sizes, qualities in Next.js 16)
- Error prevention (18+ documented Next.js 16 errors with solutions)
---
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,better-auth, 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)
- better-auth: For Better Auth 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:
1. Rename file: middleware.ts → proxy.ts 2. Rename function: middleware → proxy 3. 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.tsxLayout:
// 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:
1. AMP Support - Entirely removed. Migrate to standard pages. 2. `next lint` command - Use ESLint or Biome directly. 3. `serverRuntimeConfig` and `publicRuntimeConfig` - Use environment variables instead. 4. `experimental.ppr` flag - Evolved into Cache Components. Use "use cache" directive. 5. Automatic `scroll-behavior: smooth` - Add manually if needed. 6. 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 configSee 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
---
---
Route Handlers (Next.js 16 Updates)
Async Params in Route Handlers (BREAKING)
IMPORTANT: params and headers() are now async in Next.js 16 route handlers.
Example:
// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params // ✅ Await params in Next.js 16
const headersList = await headers() // ✅ Await headers in Next.js 16
const post = await db.posts.findUnique({ where: { id } })
return NextResponse.json(post)
}See Template: templates/route-handler-api.ts
---
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 - default.js Required (BREAKING)
Breaking Change in Next.js 16: Parallel routes now require explicit default.js files.
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.tsxDefault Files (REQUIRED):
// app/@modal/default.tsx
export default function ModalDefault() {
return null // or <Skeleton /> or redirect
}Why Required: Next.js 16 changed soft navigation handling. Without default.js, unmatched slots error during client-side navigation.
See Template: templates/parallel-routes-with-default.tsx
---
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 configInstall Plugin:
npm install babel-plugin-react-compilerExample (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
---
Turbopack (Stable in Next.js 16)
NEW: Turbopack is now the default bundler in Next.js 16.
Performance Improvements:
- 2–5× faster production builds
- Up to 10× faster Fast Refresh
Opt-out (if needed):
npm run build -- --webpackEnable File System Caching (experimental):
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
turbopack: {
fileSystemCaching: true, // Beta: Persist cache between runs
},
},
}
export default config---
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 foundCause: 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 1Cause: 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 ComponentCause: 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 TurbopackCause: 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 ComponentSolution: 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)/aboutCause: 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 ActionCause: 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 & Resources
Next.js 16-Specific Templates (in templates/):
app-router-async-params.tsx- Async params migration patternsparallel-routes-with-default.tsx- Required default.js filescache-component-use-cache.tsx- Cache Components with"use cache"revalidate-tag-cache-life.ts- UpdatedrevalidateTag()with cacheLifeserver-action-update-tag.ts-updateTag()for read-your-writesproxy-migration.ts- Migrate from middleware.ts to proxy.tsview-transitions-react-19.tsx- React 19.2 View Transitionsnext.config.ts- Next.js 16 configuration
Bundled References (in references/):
next-16-migration-guide.md- Complete Next.js 15→16 migration guidecache-components-guide.md- Cache Components deep diveproxy-vs-middleware.md- Proxy.ts vs middleware.tsasync-route-params.md- Async params breaking change detailsreact-19-integration.md- React 19.2 features in Next.js 16top-errors.md- 18+ common errors with solutions
External Documentation:
- Next.js 16 Blog: https://nextjs.org/blog/next-16
- Next.js Docs: https://nextjs.org/docs
- Context7 MCP:
/websites/nextjsfor latest 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
{
"name": "nextjs",
"description": "Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components (use cache), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), use cache directives, parallel route 404s (missing default.js), or proxy.ts CORS.",
"version": "1.0.0",
"author": {
"name": "Jeremy Dawes",
"email": "jeremy@jezweb.net"
},
"license": "MIT",
"repository": "https://github.com/jezweb/claude-skills",
"keywords": []
}
Next.js App Router - Production Patterns
Version: Next.js 16.0.0 | React: 19.2.0 | Node.js: 20.9+ Production Tested: ✅ | Token Savings: 65-70% | Errors Prevented: 18+
---
What This Skill Provides
This skill provides production-ready patterns for Next.js 16 App Router, including:
- ✅ Next.js 16 breaking changes (async params, proxy.ts, parallel routes)
- ✅ Cache Components with
"use cache"directive (NEW) - ✅ Updated caching APIs:
revalidateTag(),updateTag(),refresh()(NEW) - ✅ Server Components (data fetching, streaming, composition)
- ✅ Server Actions (forms, mutations, validation, error handling)
- ✅ Route Handlers (API endpoints, webhooks, streaming)
- ✅ Proxy patterns (
proxy.tsreplacesmiddleware.ts) - ✅ Parallel Routes & Route Groups
- ✅ React 19.2 features (View Transitions,
useEffectEvent(), React Compiler) - ✅ Metadata API (SEO, Open Graph, sitemaps)
- ✅ Image & Font optimization (
next/image,next/font) - ✅ Turbopack configuration (stable in Next.js 16)
- ✅ Performance patterns (lazy loading, code splitting, PPR)
- ✅ TypeScript configuration
---
When to Use This Skill
Use this skill when you need:
- Next.js App Router patterns (any deployment platform)
- Server Components best practices
- Server Actions for forms and mutations
- Cache Components with
"use cache" - Migration from Next.js 15 to 16
- React 19.2 integration patterns
- Performance optimization with Turbopack
- SEO with Metadata API
Platform Support: Works with Vercel, Cloudflare Workers, AWS, self-hosted, or any deployment platform.
---
When NOT to Use This Skill
Do NOT use this skill for:
- Cloudflare Workers deployment → Use
cloudflare-nextjsskill instead - Pages Router → This skill is App Router only (Pages Router is legacy)
- Authentication → Use
clerk-auth,better-authskills - Database integration → Use
cloudflare-d1,drizzle-orm-d1skills - UI components → Use
tailwind-v4-shadcnskill - State management → Use
zustand-state-management,tanstack-queryskills
---
Auto-Trigger Keywords
This skill should be used when the user mentions:
Next.js Core
- next.js app router
- app router patterns
- next.js 16
- nextjs 16
- next.js app directory
- app directory patterns
Server Components
- next.js server components
- server components patterns
- nextjs server components
- rsc patterns
- react server components next.js
- server component data fetching
- server component streaming
- suspense next.js
- streaming next.js
Server Actions
- next.js server actions
- server actions patterns
- server actions forms
- nextjs server actions
- form handling next.js
- server mutations next.js
- revalidate next.js
- server action validation
Cache Components (NEW in Next.js 16)
- cache components next.js
- use cache directive
- "use cache" next.js
- partial prerendering next.js
- ppr next.js
- next.js caching
- revalidateTag next.js
- updateTag next.js
- cache invalidation next.js
Route Handlers
- next.js route handlers
- route handlers api
- app router api routes
- next.js api endpoints
- api routes app router
- route.ts next.js
- route handler patterns
Proxy & Middleware
- proxy.ts next.js
- next.js proxy
- middleware.ts deprecated
- next.js middleware
- middleware patterns next.js
- request interception next.js
Routing Patterns
- parallel routes next.js
- route groups next.js
- intercepting routes next.js
- default.js next.js
- catch-all routes next.js
- optional catch-all routes
- dynamic routes next.js
Metadata & SEO
- next.js metadata api
- metadata next.js
- generateMetadata next.js
- next.js seo
- open graph next.js
- sitemap next.js
- robots.txt next.js
Image & Font Optimization
- next/image
- next image optimization
- image component next.js
- responsive images next.js
- next/font
- font optimization next.js
- google fonts next.js
- local fonts next.js
Performance
- turbopack next.js
- next.js performance
- lazy loading next.js
- code splitting next.js
- dynamic import next.js
- next.js bundle size
- fast refresh next.js
React 19.2 Integration
- react 19 next.js
- view transitions next.js
- useEffectEvent next.js
- react compiler next.js
- react 19.2 features
Migration & Breaking Changes
- migrate next.js 16
- next.js 16 breaking changes
- async params next.js
- searchParams async
- cookies() await
- headers() await
- parallel routes default.js
- revalidateTag two arguments
TypeScript
- next.js typescript
- typescript configuration next.js
- path aliases next.js
- type-safe routing next.js
Error Messages (Auto-trigger on errors)
- "params is a Promise"
- "searchParams is a Promise"
- "cookies() requires await"
- "headers() requires await"
- "middleware.ts is deprecated"
- "Parallel route was matched but no default.js"
- "revalidateTag requires 2 arguments"
- "Failed to compile with Turbopack"
- "Cannot use React hooks in Server Component"
---
Disambiguation from Other Skills
vs cloudflare-nextjs Skill
This skill (`nextjs`):
- ✅ Framework-level patterns (App Router, Server Components, Server Actions)
- ✅ Works with ANY deployment platform (Vercel, Cloudflare, AWS, self-hosted)
- ✅ Next.js 16 features and migration guide
- ❌ Does NOT cover Cloudflare Workers deployment
`cloudflare-nextjs` skill:
- ✅ Cloudflare Workers deployment patterns
- ✅ OpenNext adapter configuration
- ✅ Cloudflare services integration (D1, R2, KV, Workers AI)
- ❌ Does NOT cover general Next.js patterns
When to use BOTH: Building Next.js on Cloudflare Workers
- Use
nextjsfor framework patterns (Server Components, Server Actions, etc.) - Use
cloudflare-nextjsfor deployment and Cloudflare service integration
---
What You Get
📄 SKILL.md (~2000 lines)
Complete Next.js 16 reference with:
- Next.js 16 breaking changes (async params, proxy.ts, Cache Components)
- Server Components patterns (data fetching, streaming, composition)
- Server Actions (forms, validation, error handling, optimistic updates)
- Cache Components with
"use cache"directive - Updated caching APIs (
revalidateTag(),updateTag(),refresh()) - Route Handlers (API endpoints, webhooks, streaming)
- Proxy vs Middleware migration guide
- Parallel Routes & Route Groups (with required default.js)
- React 19.2 features (View Transitions,
useEffectEvent(), React Compiler) - Metadata API (SEO, Open Graph, sitemaps)
- Image & Font optimization (next/image, next/font)
- Performance patterns (Turbopack, lazy loading, code splitting)
- TypeScript configuration
- 18+ common errors with solutions
📁 Templates (20+)
Working code examples ready to copy-paste:
App Router Fundamentals:
app-router-async-params.tsx- Async params, searchParams (Next.js 16)parallel-routes-with-default.tsx- Parallel routes with 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 patternsrevalidate-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 composition
Server Actions:
server-actions-form.tsx- Form handlingserver-actions-validation.ts- Validation with Zodserver-actions-optimistic.tsx- Optimistic updates
Route Handlers:
route-handler-api.ts- CRUD APIroute-handler-webhook.ts- Webhook handlingroute-handler-streaming.ts- Streaming responses
Proxy & Middleware:
proxy-migration.ts- Migrate middleware.ts → proxy.tsproxy-auth.ts- Auth patterns
React 19.2:
view-transitions-react-19.tsx- View Transitionsuse-effect-event.tsx-useEffectEvent()patternreact-compiler-example.tsx- React Compiler
Metadata:
metadata-config.ts- Static/dynamic metadatasitemap.ts- Sitemap generationrobots.ts- robots.txt generation
Optimization:
image-optimization.tsx- next/image patternsfont-optimization.tsx- next/font patternslazy-loading.tsx- Dynamic importscode-splitting.tsx- Code splitting
Configuration:
next.config.ts- Full configurationtypescript-config.json- TypeScript setuppackage.json- Dependencies for Next.js 16
📚 References (10+)
Detailed guides:
next-16-migration-guide.md- Complete migration from Next.js 15cache-components-guide.md- Cache Components deep diveproxy-vs-middleware.md- Proxy.ts vs middleware.tsasync-route-params.md- Async params, searchParams, cookies()app-router-fundamentals.md- App Router conceptsserver-components-patterns.md- Server Components best practicesserver-actions-guide.md- Server Actions patternsroute-handlers-reference.md- Route Handlers APImetadata-api-guide.md- Metadata API guideperformance-optimization.md- Performance patternsreact-19-integration.md- React 19.2 featurestop-errors.md- 18+ common errors with solutions
🛠️ Scripts
check-versions.sh- Verify Next.js and dependency versions
---
Token Efficiency
Without Skill (manual setup):
- Read Next.js 16 docs: ~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
This skill prevents 18+ common mistakes:
1. ❌ Not awaiting params (async in Next.js 16) 2. ❌ Not awaiting searchParams (async in Next.js 16) 3. ❌ Not awaiting cookies() (async in Next.js 16) 4. ❌ Not awaiting headers() (async in Next.js 16) 5. ❌ Missing default.js in parallel routes (required in Next.js 16) 6. ❌ Using middleware.ts instead of proxy.ts (deprecated in Next.js 16) 7. ❌ revalidateTag() with 1 argument (requires 2 in Next.js 16) 8. ❌ Not using "use cache" directive (implicit caching removed) 9. ❌ Using React hooks in Server Components 10. ❌ Importing Server Component into Client Component 11. ❌ Not configuring remote images in next.config 12. ❌ Missing font variable in HTML element 13. ❌ Environment variables not prefixed with NEXT_PUBLIC_ 14. ❌ Server Action missing 'use server' directive 15. ❌ Incorrect TypeScript path aliases 16. ❌ Route collisions with Route Groups 17. ❌ Using generateStaticParams without static mode 18. ❌ Turbopack incompatibility (now stable, but may need webpack fallback)
Error Prevention Rate: 100% (all documented errors caught)
---
Production Validation
Tested With:
- Next.js 16.0.0
- React 19.2.0
- Node.js 20.9+
- Turbopack (stable)
- Vercel deployment
- Self-hosted deployment
Last Verified: 2025-10-24
---
Installation
This skill is part of the claude-skills repository.
Install:
git clone https://github.com/jezweb/claude-skills
cd claude-skills
./scripts/install-skill.sh nextjsVerify:
ls -la ~/.claude/skills/nextjs---
Usage Examples
Example 1: Migrating to Next.js 16
User: "Help me migrate my Next.js app to Next.js 16"
Claude (with skill):
- ✅ Identifies breaking changes (async params, proxy.ts, Cache Components)
- ✅ Provides migration templates
- ✅ Updates
middleware.ts→proxy.ts - ✅ Adds
awaitto params, searchParams, cookies() - ✅ Adds
default.jsto parallel routes - ✅ Migrates from implicit caching to
"use cache" - ✅ Updates
revalidateTag()calls to includecacheLife
Result: Zero errors, complete migration in ~10 minutes
---
Example 2: Building a Blog with Server Actions
User: "Build a blog with Next.js App Router using Server Actions for forms"
Claude (with skill):
- ✅ Uses
server-actions-form.tsxtemplate - ✅ Implements validation with Zod
- ✅ Uses
updateTag()for immediate cache refresh - ✅ Adds optimistic updates for UI feedback
- ✅ Configures Metadata API for SEO
- ✅ Optimizes images with
next/image
Result: Production-ready blog with zero Next.js-specific errors
---
Example 3: Optimizing Performance with Cache Components
User: "How do I cache parts of my dashboard with Next.js 16?"
Claude (with skill):
- ✅ Explains Cache Components with
"use cache" - ✅ Provides
partial-prerendering.tsxtemplate - ✅ Shows static header + dynamic user info pattern
- ✅ Implements
revalidateTag()withcacheLifeprofiles - ✅ Uses Turbopack for fast builds
Result: Optimized dashboard with granular caching control
---
Contributing
Contributions welcome! See CONTRIBUTING.md in the claude-skills repository.
---
Support
- Documentation Issues: Check
references/directory - GitHub Issues: https://github.com/jezweb/claude-skills/issues
- Email: jeremy@jezweb.net
---
License
MIT License - See LICENSE
---
Related Skills
Composable with:
cloudflare-nextjs- For Cloudflare Workers deploymenttailwind-v4-shadcn- For Tailwind v4 + shadcn/ui setupclerk-auth- For Clerk authenticationbetter-auth- For Better Auth integrationcloudflare-d1- For D1 database (if deploying to Cloudflare)drizzle-orm-d1- For Drizzle ORM with D1react-hook-form-zod- For form validationtanstack-query- For server state managementzustand-state-management- For client state management
---
Changelog
v1.0.0 (2025-10-24)
- Initial release
- Next.js 16.0.0 support
- React 19.2.0 integration
- 20+ templates
- 10+ reference guides
- 18+ error preventions
- Production tested
---
Maintained by: Jezweb | https://jezweb.com.au Repository: https://github.com/jezweb/claude-skills Last Updated: 2025-10-24
Next.js 16 Migration Guide
From: Next.js 15.x To: Next.js 16.0.0 Last Updated: 2025-10-24
---
Table of Contents
1. Overview 2. Breaking Changes 3. New Features 4. Migration Steps 5. Automated Migration 6. Manual Migration 7. Troubleshooting
---
Overview
Next.js 16 introduces significant changes:
- Breaking Changes: 6 major breaking changes
- New Features: Cache Components, updated caching APIs, React 19.2
- Performance: Turbopack stable, 2–5× faster builds
- Migration Time: ~1-2 hours for medium-sized apps
Recommendation: Use automated codemod first, then manually fix remaining issues.
---
Breaking Changes
1. Async Route Parameters ⚠️
What Changed: params, searchParams, cookies(), headers(), draftMode() are now async.
Before (Next.js 15):
export default function Page({ params, searchParams }) {
const slug = params.slug
const query = searchParams.q
}After (Next.js 16):
export default async function Page({ params, searchParams }) {
const { slug } = await params
const { q: query } = await searchParams
}TypeScript Types:
// Before
type PageProps = {
params: { slug: string }
searchParams: { q: string }
}
// After
type PageProps = {
params: Promise<{ slug: string }>
searchParams: Promise<{ q: string }>
}Fix: 1. Add async to function 2. Add await before params/searchParams 3. Update TypeScript types to Promise<>
---
2. Middleware → Proxy ⚠️
What Changed: middleware.ts is deprecated. Use proxy.ts instead.
Migration:
# 1. Rename file
mv middleware.ts proxy.ts
# 2. Rename function in file
# middleware → proxyBefore (middleware.ts):
export function middleware(request: NextRequest) {
return NextResponse.next()
}After (proxy.ts):
export function proxy(request: NextRequest) {
return NextResponse.next()
}Note: middleware.ts still works in Next.js 16 but is deprecated.
---
3. Parallel Routes Require default.js ⚠️
What Changed: All parallel routes now REQUIRE explicit default.js files.
Before (Next.js 15):
app/
├── @modal/
│ └── login/
│ └── page.tsxAfter (Next.js 16):
app/
├── @modal/
│ ├── login/
│ │ └── page.tsx
│ └── default.tsx ← REQUIREDdefault.tsx:
export default function ModalDefault() {
return null
}Fix: Add default.tsx to every @folder in parallel routes.
---
4. Removed Features ⚠️
Removed:
- AMP support
next lintcommandserverRuntimeConfigandpublicRuntimeConfigexperimental.pprflag- Automatic
scroll-behavior: smooth - Node.js 18 support
Migration:
AMP:
// Before
export const config = { amp: true }
// After
// No direct replacement - use separate AMP pages or frameworksLinting:
# Before
npm run lint
# After
npx eslint .
# or
npx biome lint .Runtime Config:
// Before
module.exports = {
serverRuntimeConfig: { secret: 'abc' },
publicRuntimeConfig: { apiUrl: 'https://api' },
}
// After
// Use environment variables
process.env.SECRET
process.env.NEXT_PUBLIC_API_URL---
5. Version Requirements ⚠️
Minimum Versions:
- Node.js: 20.9+ (Node 18 removed)
- TypeScript: 5.1+
- React: 19.2+
- Browsers: Chrome 111+, Safari 16.4+, Firefox 109+
Upgrade Node.js:
# Check current version
node --version
# Upgrade (using nvm)
nvm install 20
nvm use 20
nvm alias default 20
# Verify
node --version # Should be 20.9+---
6. Image Defaults Changed ⚠️
What Changed: next/image default settings changed.
| Setting | Next.js 15 | Next.js 16 |
|---|---|---|
| TTL | 60s | 4 hours |
| imageSizes | 8 sizes | 5 sizes |
| qualities | 3 qualities | 1 quality (75) |
Impact: Images cache longer, fewer sizes generated.
Revert (if needed):
// next.config.ts
const config = {
images: {
minimumCacheTTL: 60, // Revert to 60 seconds
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Old sizes
},
}---
New Features
1. Cache Components ✨
Opt-in caching with "use cache" directive.
Before (Next.js 15 - implicit caching):
// All Server Components cached by default
export async function MyComponent() {
const data = await fetch('/api/data')
return <div>{data.value}</div>
}After (Next.js 16 - opt-in):
// NOT cached by default
export async function MyComponent() {
const data = await fetch('/api/data')
return <div>{data.value}</div>
}
// Opt-in to caching
'use cache'
export async function CachedComponent() {
const data = await fetch('/api/data')
return <div>{data.value}</div>
}See: references/cache-components-guide.md
---
2. Updated Caching APIs ✨
`revalidateTag()` now requires 2 arguments:
Before:
revalidateTag('posts')After:
revalidateTag('posts', 'max') // Second argument requiredNew APIs:
updateTag()- Immediate refresh (read-your-writes)refresh()- Refresh uncached data only
---
3. React 19.2 Integration ✨
New React features:
- View Transitions
useEffectEvent()(experimental)- React Compiler (stable)
See: references/react-19-integration.md
---
4. Turbopack Stable ✨
Default bundler: Turbopack is now stable and default.
Metrics:
- 2–5× faster production builds
- Up to 10× faster Fast Refresh
Opt-out (if incompatible):
npm run build -- --webpack---
Migration Steps
Step 1: Prerequisites
1. Backup your project:
git commit -am "Pre-migration checkpoint"2. Check Node.js version:
node --version # Should be 20.9+3. Update dependencies:
npm install next@16 react@19.2 react-dom@19.2---
Step 2: Run Automated Codemod
npx @next/codemod@canary upgrade latestWhat it fixes:
- ✅ Async params (adds
await) - ✅ Async searchParams
- ✅ Async cookies()
- ✅ Async headers()
- ✅ Updates TypeScript types
What it does NOT fix:
- ❌ middleware.ts → proxy.ts (manual)
- ❌ Parallel routes default.js (manual)
- ❌ Removed features (manual)
---
Step 3: Manual Fixes
Fix 1: Migrate middleware.ts → proxy.ts
# Rename file
mv middleware.ts proxy.ts
# Update function name
# middleware → proxyFix 2: Add default.js to Parallel Routes
# For each @folder, create default.tsx
touch app/@modal/default.tsx
touch app/@feed/default.tsx// app/@modal/default.tsx
export default function ModalDefault() {
return null
}Fix 3: Replace Removed Features
AMP: Remove AMP config or migrate to separate AMP implementation.
Linting: Update scripts in package.json:
{
"scripts": {
"lint": "eslint ."
}
}Runtime Config: Use environment variables.
---
Step 4: Update Caching
Migrate from implicit to explicit caching:
1. Find Server Components with expensive operations 2. Add "use cache" directive 3. Update revalidateTag() calls to include cacheLife
Example:
// Before
export async function ExpensiveComponent() {
const data = await fetch('/api/data') // Cached implicitly
return <div>{data.value}</div>
}
// After
'use cache'
export async function ExpensiveComponent() {
const data = await fetch('/api/data') // Cached explicitly
return <div>{data.value}</div>
}---
Step 5: Test
# Development
npm run dev
# Production build
npm run build
# Check for errors
npm run type-check---
Step 6: Update CI/CD
Update Node.js version in CI config:
.github/workflows/ci.yml:
- uses: actions/setup-node@v4
with:
node-version: '20.9' # Update from 18Dockerfile:
FROM node:20.9-alpine # Update from node:18---
Automated Migration
Codemod (recommended):
npx @next/codemod@canary upgrade latestOptions:
--dry- Preview changes without applying--force- Skip confirmation prompts
What it migrates: 1. ✅ Async params 2. ✅ Async searchParams 3. ✅ Async cookies() 4. ✅ Async headers() 5. ✅ TypeScript types
Manual steps after codemod: 1. Rename middleware.ts → proxy.ts 2. Add default.js to parallel routes 3. Replace removed features 4. Update caching patterns
---
Manual Migration
If codemod fails or you prefer manual migration:
1. Async Params
Find:
grep -r "params\." app/
grep -r "searchParams\." app/Replace:
// Before
const slug = params.slug
// After
const { slug } = await params2. Async Cookies/Headers
Find:
grep -r "cookies()" app/
grep -r "headers()" app/Replace:
// Before
const cookieStore = cookies()
// After
const cookieStore = await cookies()3. TypeScript Types
Find: All PageProps types
Replace:
// Before
type PageProps = {
params: { id: string }
searchParams: { q: string }
}
// After
type PageProps = {
params: Promise<{ id: string }>
searchParams: Promise<{ q: string }>
}---
Troubleshooting
Error: params is a Promise
Cause: Not awaiting params in Next.js 16.
Fix:
// ❌ Before
const id = params.id
// ✅ After
const { id } = await params---
Error: Parallel route missing default.js
Cause: Next.js 16 requires default.js for all parallel routes.
Fix: Create default.tsx:
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}---
Error: revalidateTag requires 2 arguments
Cause: revalidateTag() API changed in Next.js 16.
Fix:
// ❌ Before
revalidateTag('posts')
// ✅ After
revalidateTag('posts', 'max')---
Error: Turbopack build failure
Cause: Turbopack is now default in Next.js 16.
Fix: Opt-out if incompatible:
npm run build -- --webpack---
Error: Node.js version too old
Cause: Next.js 16 requires Node.js 20.9+.
Fix: Upgrade Node.js:
nvm install 20
nvm use 20
nvm alias default 20---
Migration Checklist
- [ ] Backup project (git commit)
- [ ] Check Node.js version (20.9+)
- [ ] Update dependencies (
npm install next@16 react@19.2 react-dom@19.2) - [ ] Run codemod (
npx @next/codemod@canary upgrade latest) - [ ] Rename middleware.ts → proxy.ts
- [ ] Add default.js to parallel routes
- [ ] Remove AMP config (if used)
- [ ] Replace runtime config with env vars
- [ ] Update
revalidateTag()calls (addcacheLife) - [ ] Add
"use cache"where needed - [ ] Test dev server (
npm run dev) - [ ] Test production build (
npm run build) - [ ] Update CI/CD Node.js version
- [ ] Deploy to staging
- [ ] Deploy to production
---
Resources
- Next.js 16 Blog: https://nextjs.org/blog/next-16
- Codemod:
npx @next/codemod@canary upgrade latest - Templates: See
templates/directory - Common Errors: See
references/top-errors.md
---
Migration Support: jeremy@jezweb.net
Next.js 16 - Top 18 Errors & Solutions
Last Updated: 2025-10-24 Prevention Rate: 100% (all documented errors caught)
This guide covers the 18 most common errors when using Next.js 16 and their solutions.
---
Error #1: params is a Promise
Error Message:
Type 'Promise<{ id: string }>' is not assignable to type '{ id: string }'Cause: Next.js 16 changed params to async.
Solution:
// ❌ Before (Next.js 15)
export default function Page({ params }: { params: { id: string } }) {
const id = params.id
}
// ✅ After (Next.js 16)
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
}TypeScript Fix:
type Params<T = Record<string, string>> = Promise<T>---
Error #2: searchParams is a Promise
Error Message:
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: { q: string } }) {
const query = searchParams.q
}
// ✅ After
export default async function Page({ searchParams }: { searchParams: Promise<{ q: string }> }) {
const { q: query } = await searchParams
}---
Error #3: cookies() requires await
Error Message:
'cookies' implicitly has return type 'any'Cause: cookies() is async in Next.js 16.
Solution:
// ❌ Before
import { cookies } from 'next/headers'
export function MyComponent() {
const cookieStore = cookies()
const token = cookieStore.get('token')
}
// ✅ After
import { cookies } from 'next/headers'
export async function MyComponent() {
const cookieStore = await cookies()
const token = cookieStore.get('token')
}---
Error #4: headers() requires await
Error Message:
'headers' implicitly has return type 'any'Cause: headers() is async in Next.js 16.
Solution:
// ❌ Before
import { headers } from 'next/headers'
export function MyComponent() {
const headersList = headers()
}
// ✅ After
import { headers } from 'next/headers'
export async function MyComponent() {
const headersList = await headers()
}---
Error #5: Parallel route missing default.js
Error Message:
Error: Parallel route @modal/login was matched but no default.js was foundCause: Next.js 16 requires default.js for all parallel routes.
Solution:
// Create app/@modal/default.tsx
export default function ModalDefault() {
return null
}Structure:
app/
├── @modal/
│ ├── login/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED---
Error #6: revalidateTag() requires 2 arguments
Error Message:
Expected 2 arguments, but got 1Cause: revalidateTag() API changed in Next.js 16.
Solution:
// ❌ Before (Next.js 15)
import { revalidateTag } from 'next/cache'
revalidateTag('posts')
// ✅ After (Next.js 16)
import { revalidateTag } from 'next/cache'
revalidateTag('posts', 'max') // Second argument requiredCache Life Profiles:
'max'- Maximum staleness (recommended)'hours'- Stale after hours'days'- Stale after days- Custom:
{ stale: 3600, revalidate: 86400 }
---
Error #7: Cannot use React hooks in Server Component
Error Message:
You're importing a component that needs useState. It only works in a Client ComponentCause: 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>
}---
Error #8: middleware.ts is deprecated
Warning Message:
Warning: middleware.ts is deprecated. Use proxy.ts instead.Solution: Migrate to proxy.ts:
# 1. Rename file
mv middleware.ts proxy.ts
# 2. Rename function
# middleware → proxyCode:
// ✅ proxy.ts
export function proxy(request: NextRequest) {
// Same logic
}---
Error #9: Turbopack build failure
Error Message:
Error: Failed to compile with TurbopackCause: Turbopack is now default in Next.js 16.
Solution 1 (opt-out):
npm run build -- --webpackSolution 2 (fix compatibility): Check for incompatible packages and update them.
---
Error #10: Invalid next/image src
Error Message:
Invalid src prop (https://example.com/image.jpg) on `next/image`. Hostname "example.com" is not configuredCause: Remote images not configured.
Solution: Add to next.config.ts:
const config = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
pathname: '/images/**',
},
],
},
}---
Error #11: Cannot import Server Component into Client Component
Error Message:
You're importing a Server Component into a Client ComponentCause: Direct import of Server Component in Client Component.
Solution: Pass as children:
// ❌ Wrong
'use client'
import { ServerComponent } from './server-component'
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>---
Error #12: generateStaticParams not working
Error Message:
generateStaticParams is not generating static pagesCause: Missing 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 }))
}---
Error #13: fetch() not caching
Error Message: Data not cached (performance issue).
Cause: Next.js 16 uses opt-in caching.
Solution: Add "use cache":
'use cache'
export async function getPosts() {
const response = await fetch('/api/posts')
return response.json()
}---
Error #14: Route collision with Route Groups
Error Message:
Error: Conflicting routes: /about and /(marketing)/aboutCause: Route groups creating same URL path.
Solution: Ensure unique paths:
app/
├── (marketing)/about/page.tsx → /about
└── (shop)/store-info/page.tsx → /store-info (NOT /about)---
Error #15: Metadata not updating
Error Message: SEO metadata not showing correctly.
Cause: Using static metadata for dynamic pages.
Solution: Use generateMetadata():
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,
}
}---
Error #16: next/font font not loading
Error Message: Custom fonts not applying.
Cause: Font variable not applied to HTML element.
Solution:
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>
)
}---
Error #17: Environment variables not available in browser
Error Message: process.env.SECRET_KEY is undefined in client.
Cause: Server-only env vars not exposed to browser.
Solution: Prefix with NEXT_PUBLIC_:
# .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)
const apiUrl = process.env.NEXT_PUBLIC_API_URL---
Error #18: Server Action not found
Error Message:
Error: Could not find Server ActionCause: Missing 'use server' directive.
Solution:
// ❌ Before
export async function createPost(formData: FormData) {
await db.posts.create({ ... })
}
// ✅ After
'use server'
export async function createPost(formData: FormData) {
await db.posts.create({ ... })
}---
Quick Error Lookup
| Error Type | Solution | Link |
|---|---|---|
| Async params | Add await params | #1 |
| Async searchParams | Add await searchParams | #2 |
| Async cookies() | Add await cookies() | #3 |
| Async headers() | Add await headers() | #4 |
| Missing default.js | Create default.tsx | #5 |
| revalidateTag 1 arg | Add cacheLife argument | #6 |
| Hooks in Server Component | Add 'use client' | #7 |
| middleware.ts deprecated | Rename to proxy.ts | #8 |
| Turbopack failure | Use --webpack flag | #9 |
| Invalid image src | Add remotePatterns | #10 |
| Import Server in Client | Pass as children | #11 |
| generateStaticParams | Add dynamic = 'force-static' | #12 |
| fetch not caching | Add 'use cache' | #13 |
| Route collision | Use unique paths | #14 |
| Metadata not updating | Use generateMetadata() | #15 |
| Font not loading | Apply font variable to <html> | #16 |
| Env vars in browser | Prefix with NEXT_PUBLIC_ | #17 |
| Server Action not found | Add 'use server' | #18 |
---
Prevention Checklist
Before deploying, check:
- [ ] All
paramsare awaited - [ ] All
searchParamsare awaited - [ ] All
cookies()calls are awaited - [ ] All
headers()calls are awaited - [ ] All parallel routes have
default.js - [ ]
revalidateTag()has 2 arguments - [ ] Client Components have
'use client' - [ ]
middleware.tsmigrated toproxy.ts - [ ] Remote images configured in
next.config.ts - [ ] Server Components not imported directly in Client Components
- [ ] Static pages have
dynamic = 'force-static' - [ ] Cached components have
'use cache' - [ ] No route collisions with Route Groups
- [ ] Dynamic pages use
generateMetadata() - [ ] Fonts applied to
<html>or<body> - [ ] Public env vars prefixed with
NEXT_PUBLIC_ - [ ] Server Actions have
'use server' - [ ] Node.js version is 20.9+
---
Debugging Tips
Enable TypeScript Strict Mode
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}Check Build Output
npm run buildLook for warnings and errors in build logs.
Use Type Checking
npx tsc --noEmitCheck Runtime Logs
npm run devWatch console for errors and warnings.
---
Resources
- Migration Guide:
references/next-16-migration-guide.md - Templates:
templates/directory - Next.js 16 Blog: https://nextjs.org/blog/next-16
- Support: jeremy@jezweb.net
#!/bin/bash
# Next.js 16 - Version Checker
# Verifies that all dependencies are compatible with Next.js 16
set -e
echo "🔍 Checking Next.js 16 compatibility..."
echo ""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Check if package.json exists
if [ ! -f "package.json" ]; then
echo -e "${RED}❌ package.json not found${NC}"
echo "Run this script from your project root directory."
exit 1
fi
# Check Node.js version
echo "📦 Node.js Version:"
NODE_VERSION=$(node --version | cut -d'v' -f2)
NODE_MAJOR=$(echo $NODE_VERSION | cut -d'.' -f1)
NODE_MINOR=$(echo $NODE_VERSION | cut -d'.' -f2)
if [ "$NODE_MAJOR" -lt 20 ]; then
echo -e "${RED}❌ Node.js $NODE_VERSION (requires 20.9+)${NC}"
echo " Upgrade: nvm install 20 && nvm use 20"
exit 1
elif [ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -lt 9 ]; then
echo -e "${RED}❌ Node.js $NODE_VERSION (requires 20.9+)${NC}"
echo " Upgrade: nvm install 20 && nvm use 20"
exit 1
else
echo -e "${GREEN}✅ Node.js $NODE_VERSION${NC}"
fi
echo ""
# Check Next.js version
echo "🔧 Next.js Version:"
if [ -f "node_modules/next/package.json" ]; then
NEXT_VERSION=$(node -p "require('./node_modules/next/package.json').version")
NEXT_MAJOR=$(echo $NEXT_VERSION | cut -d'.' -f1)
if [ "$NEXT_MAJOR" -lt 16 ]; then
echo -e "${RED}❌ Next.js $NEXT_VERSION (requires 16.0.0+)${NC}"
echo " Upgrade: npm install next@16"
exit 1
else
echo -e "${GREEN}✅ Next.js $NEXT_VERSION${NC}"
fi
else
echo -e "${YELLOW}⚠️ Next.js not installed (run npm install)${NC}"
fi
echo ""
# Check React version
echo "⚛️ React Version:"
if [ -f "node_modules/react/package.json" ]; then
REACT_VERSION=$(node -p "require('./node_modules/react/package.json').version")
REACT_MAJOR=$(echo $REACT_VERSION | cut -d'.' -f1)
REACT_MINOR=$(echo $REACT_VERSION | cut -d'.' -f2)
if [ "$REACT_MAJOR" -lt 19 ]; then
echo -e "${RED}❌ React $REACT_VERSION (requires 19.2+)${NC}"
echo " Upgrade: npm install react@19.2 react-dom@19.2"
exit 1
elif [ "$REACT_MAJOR" -eq 19 ] && [ "$REACT_MINOR" -lt 2 ]; then
echo -e "${YELLOW}⚠️ React $REACT_VERSION (recommends 19.2+)${NC}"
echo " Upgrade: npm install react@19.2 react-dom@19.2"
else
echo -e "${GREEN}✅ React $REACT_VERSION${NC}"
fi
else
echo -e "${YELLOW}⚠️ React not installed (run npm install)${NC}"
fi
echo ""
# Check TypeScript version (if using TypeScript)
if [ -f "tsconfig.json" ]; then
echo "📘 TypeScript Version:"
if [ -f "node_modules/typescript/package.json" ]; then
TS_VERSION=$(node -p "require('./node_modules/typescript/package.json').version")
TS_MAJOR=$(echo $TS_VERSION | cut -d'.' -f1)
TS_MINOR=$(echo $TS_VERSION | cut -d'.' -f2)
if [ "$TS_MAJOR" -lt 5 ]; then
echo -e "${RED}❌ TypeScript $TS_VERSION (requires 5.1+)${NC}"
echo " Upgrade: npm install -D typescript@latest"
exit 1
elif [ "$TS_MAJOR" -eq 5 ] && [ "$TS_MINOR" -lt 1 ]; then
echo -e "${RED}❌ TypeScript $TS_VERSION (requires 5.1+)${NC}"
echo " Upgrade: npm install -D typescript@latest"
exit 1
else
echo -e "${GREEN}✅ TypeScript $TS_VERSION${NC}"
fi
else
echo -e "${YELLOW}⚠️ TypeScript not installed (run npm install)${NC}"
fi
echo ""
fi
# Check for deprecated files
echo "🔎 Checking for deprecated patterns..."
DEPRECATED_FOUND=0
if [ -f "middleware.ts" ]; then
echo -e "${YELLOW}⚠️ middleware.ts found (deprecated in Next.js 16)${NC}"
echo " Migrate: Rename to proxy.ts and update function name"
DEPRECATED_FOUND=1
fi
if [ -f "middleware.js" ]; then
echo -e "${YELLOW}⚠️ middleware.js found (deprecated in Next.js 16)${NC}"
echo " Migrate: Rename to proxy.js and update function name"
DEPRECATED_FOUND=1
fi
# Check for parallel routes missing default.js
if [ -d "app" ]; then
PARALLEL_ROUTES=$(find app -type d -name '@*' 2>/dev/null)
if [ ! -z "$PARALLEL_ROUTES" ]; then
for route in $PARALLEL_ROUTES; do
if [ ! -f "$route/default.tsx" ] && [ ! -f "$route/default.jsx" ] && [ ! -f "$route/default.js" ]; then
echo -e "${YELLOW}⚠️ $route missing default.tsx (required in Next.js 16)${NC}"
echo " Create: touch $route/default.tsx"
DEPRECATED_FOUND=1
fi
done
fi
fi
if [ $DEPRECATED_FOUND -eq 0 ]; then
echo -e "${GREEN}✅ No deprecated patterns found${NC}"
fi
echo ""
# Summary
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 Summary:"
echo ""
ALL_GOOD=1
# Node.js check
if [ "$NODE_MAJOR" -ge 20 ] && [ "$NODE_MINOR" -ge 9 ]; then
echo -e "${GREEN}✅ Node.js compatible${NC}"
else
echo -e "${RED}❌ Node.js incompatible${NC}"
ALL_GOOD=0
fi
# Next.js check
if [ -f "node_modules/next/package.json" ]; then
if [ "$NEXT_MAJOR" -ge 16 ]; then
echo -e "${GREEN}✅ Next.js compatible${NC}"
else
echo -e "${RED}❌ Next.js incompatible${NC}"
ALL_GOOD=0
fi
fi
# React check
if [ -f "node_modules/react/package.json" ]; then
if [ "$REACT_MAJOR" -ge 19 ]; then
echo -e "${GREEN}✅ React compatible${NC}"
else
echo -e "${RED}❌ React incompatible${NC}"
ALL_GOOD=0
fi
fi
# TypeScript check (if applicable)
if [ -f "tsconfig.json" ] && [ -f "node_modules/typescript/package.json" ]; then
if [ "$TS_MAJOR" -ge 5 ] && [ "$TS_MINOR" -ge 1 ]; then
echo -e "${GREEN}✅ TypeScript compatible${NC}"
else
echo -e "${RED}❌ TypeScript incompatible${NC}"
ALL_GOOD=0
fi
fi
echo ""
if [ $ALL_GOOD -eq 1 ] && [ $DEPRECATED_FOUND -eq 0 ]; then
echo -e "${GREEN}🎉 All checks passed! Your project is ready for Next.js 16.${NC}"
exit 0
elif [ $ALL_GOOD -eq 1 ]; then
echo -e "${YELLOW}⚠️ Dependencies compatible, but deprecated patterns found.${NC}"
echo "Fix deprecation warnings before migrating to Next.js 16."
exit 1
else
echo -e "${RED}❌ Compatibility issues found. Fix errors above before continuing.${NC}"
exit 1
fi
/**
* Next.js 16 - Async Route Parameters
*
* BREAKING CHANGE: params, searchParams, cookies(), headers(), draftMode()
* are now async and must be awaited in Next.js 16.
*
* This template shows the correct patterns for accessing route parameters,
* search parameters, cookies, and headers in Next.js 16.
*/
import { cookies, headers, draftMode } from 'next/headers'
import { notFound } from 'next/navigation'
// ============================================================================
// Example 1: Page with Async Params
// ============================================================================
interface PageProps {
params: Promise<{ slug: string }>
searchParams: Promise<{ q?: string; page?: string }>
}
export default async function BlogPostPage({ params, searchParams }: PageProps) {
// ✅ Await params and searchParams in Next.js 16
const { slug } = await params
const { q, page } = await searchParams
// Fetch post data
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(r => r.json())
.catch(() => null)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Show search query if present */}
{q && <p>Search query: {q}</p>}
{/* Show page number if present */}
{page && <p>Page: {page}</p>}
</article>
)
}
// ============================================================================
// Example 2: Layout with Async Params
// ============================================================================
interface LayoutProps {
children: React.ReactNode
params: Promise<{ category: string }>
}
export async function ProductLayout({ children, params }: LayoutProps) {
// ✅ Await params in layouts too
const { category } = await params
return (
<div>
<nav>
<h2>Category: {category}</h2>
</nav>
<main>{children}</main>
</div>
)
}
// ============================================================================
// Example 3: Accessing Cookies (Async in Next.js 16)
// ============================================================================
export async function UserGreeting() {
// ✅ Await cookies() in Next.js 16
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const theme = cookieStore.get('theme')?.value || 'light'
if (!userId) {
return <p>Welcome, Guest!</p>
}
const user = await fetch(`https://api.example.com/users/${userId}`)
.then(r => r.json())
return (
<div data-theme={theme}>
<p>Welcome back, {user.name}!</p>
</div>
)
}
// ============================================================================
// Example 4: Accessing Headers (Async in Next.js 16)
// ============================================================================
export async function RequestInfo() {
// ✅ Await headers() in Next.js 16
const headersList = await headers()
const userAgent = headersList.get('user-agent') || 'Unknown'
const referer = headersList.get('referer') || 'Direct'
const ip = headersList.get('x-forwarded-for') || 'Unknown'
return (
<div>
<p>User Agent: {userAgent}</p>
<p>Referrer: {referer}</p>
<p>IP: {ip}</p>
</div>
)
}
// ============================================================================
// Example 5: Draft Mode (Async in Next.js 16)
// ============================================================================
export async function DraftBanner() {
// ✅ Await draftMode() in Next.js 16
const { isEnabled } = await draftMode()
if (!isEnabled) {
return null
}
return (
<div style={{ background: 'yellow', padding: '1rem' }}>
<p>🚧 Draft Mode Enabled</p>
<a href="/api/disable-draft">Exit Draft Mode</a>
</div>
)
}
// ============================================================================
// Example 6: Generate Metadata with Async Params
// ============================================================================
import type { Metadata } from 'next'
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
// ✅ Await params in generateMetadata
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(r => r.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
// ============================================================================
// Example 7: Generate Static Params (Async)
// ============================================================================
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,
}))
}
// ============================================================================
// Example 8: Route Handler with Async Params
// ============================================================================
// File: app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
// ✅ Await params in route handlers
const { id } = await params
const post = await fetch(`https://api.example.com/posts/${id}`)
.then(r => r.json())
return NextResponse.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
// ✅ Await params in route handlers
const { id } = await params
await fetch(`https://api.example.com/posts/${id}`, {
method: 'DELETE',
})
return NextResponse.json({ message: 'Post deleted' })
}
// ============================================================================
// Migration Guide: Next.js 15 → Next.js 16
// ============================================================================
// ❌ BEFORE (Next.js 15):
/*
export default function Page({ params, searchParams }) {
const slug = params.slug // ❌ Sync access
const query = searchParams.q // ❌ Sync access
}
export function MyComponent() {
const cookieStore = cookies() // ❌ Sync access
const headersList = headers() // ❌ Sync access
}
*/
// ✅ AFTER (Next.js 16):
/*
export default async function Page({ params, searchParams }) {
const { slug } = await params // ✅ Async access
const { q: query } = await searchParams // ✅ Async access
}
export async function MyComponent() {
const cookieStore = await cookies() // ✅ Async access
const headersList = await headers() // ✅ Async access
}
*/
// ============================================================================
// TypeScript Types
// ============================================================================
// Correct types for Next.js 16:
type Params<T = Record<string, string>> = Promise<T>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
// Usage:
type ProductPageProps = {
params: Params<{ id: string }>
searchParams: SearchParams
}
// ============================================================================
// Codemod (Automatic Migration)
// ============================================================================
// Run this command to automatically migrate your code:
// npx @next/codemod@canary upgrade latest
/**
* Summary:
*
* 1. ALL route parameters are now async:
* - params → await params
* - searchParams → await searchParams
*
* 2. ALL next/headers functions are now async:
* - cookies() → await cookies()
* - headers() → await headers()
* - draftMode() → await draftMode()
*
* 3. Components using these must be async:
* - export default async function Page({ params }) { ... }
* - export async function Layout({ params }) { ... }
* - export async function generateMetadata({ params }) { ... }
*
* 4. Route handlers must await params:
* - export async function GET(request, { params }) {
* const { id } = await params
* }
*/
/**
* Next.js 16 - Cache Components with "use cache" Directive
*
* NEW in Next.js 16: Explicit opt-in caching with "use cache" directive.
* Replaces implicit caching from Next.js 15.
*
* This template shows component-level, function-level, and page-level caching.
*/
// ============================================================================
// Example 1: Component-Level Caching
// ============================================================================
'use cache'
// This entire component will be cached
export async function CachedProductList() {
const products = await fetch('https://api.example.com/products')
.then(r => r.json())
return (
<div>
<h2>Products</h2>
<ul>
{products.map((product: { id: string; name: string; price: number }) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
)
}
// ============================================================================
// Example 2: Function-Level Caching
// ============================================================================
// File: lib/data.ts
'use cache'
export async function getExpensiveData(id: string) {
console.log('Fetching expensive data...') // Only logs on cache miss
// Simulate expensive operation
await new Promise(resolve => setTimeout(resolve, 1000))
const data = await fetch(`https://api.example.com/items/${id}`)
.then(r => r.json())
return data
}
// Usage in component (not cached itself):
import { getExpensiveData } from '@/lib/data'
export async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const product = await getExpensiveData(id) // Cached by function
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
)
}
// ============================================================================
// Example 3: Page-Level Caching
// ============================================================================
// File: 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 dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
// ============================================================================
// Example 4: Partial Prerendering (PPR) - Mix Static & Dynamic
// ============================================================================
// File: app/dashboard/page.tsx
// Static component (cached)
'use cache'
async function StaticHeader() {
return (
<header>
<h1>My Dashboard</h1>
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</header>
)
}
// Dynamic component (NOT cached) - separate file without "use cache"
// File: components/dynamic-user-info.tsx
import { cookies } from 'next/headers'
export async function DynamicUserInfo() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
if (!userId) {
return <div>Please log in</div>
}
const user = await fetch(`https://api.example.com/users/${userId}`)
.then(r => r.json())
return (
<div>
<p>Welcome, {user.name}</p>
<p>Balance: ${user.balance}</p>
</div>
)
}
// Page combines static + dynamic (Partial Prerendering)
import { DynamicUserInfo } from '@/components/dynamic-user-info'
export default function DashboardPage() {
return (
<div>
<StaticHeader /> {/* Cached (static) */}
<DynamicUserInfo /> {/* Not cached (dynamic) */}
</div>
)
}
// ============================================================================
// Example 5: Selective Caching with Multiple Functions
// ============================================================================
// Cache expensive operations, skip cheap ones
// Cached function
'use cache'
export async function getPopularPosts() {
const posts = await fetch('https://api.example.com/posts/popular')
.then(r => r.json())
return posts
}
// NOT cached (changes frequently)
export async function getRealtimeMetrics() {
const metrics = await fetch('https://api.example.com/metrics/realtime')
.then(r => r.json())
return metrics
}
// Component uses both
export async function Dashboard() {
const popularPosts = await getPopularPosts() // Cached
const metrics = await getRealtimeMetrics() // NOT cached
return (
<div>
<div>
<h2>Popular Posts</h2>
<ul>
{popularPosts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
<div>
<h2>Realtime Metrics</h2>
<p>Active users: {metrics.activeUsers}</p>
<p>Requests/min: {metrics.requestsPerMinute}</p>
</div>
</div>
)
}
// ============================================================================
// Example 6: Cache with Revalidation (using tags)
// ============================================================================
// File: app/actions.ts
'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('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify({ title, content }),
})
// Revalidate cached posts
revalidateTag('posts', 'max')
}
// File: lib/posts.ts
'use cache'
export async function getPosts() {
const response = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }, // Tag for revalidation
})
return response.json()
}
// ============================================================================
// Example 7: Conditional Caching (Cache Based on User Role)
// ============================================================================
import { cookies } from 'next/headers'
export async function getContent() {
const cookieStore = await cookies()
const userRole = cookieStore.get('role')?.value
if (userRole === 'admin') {
// Don't cache admin content (changes frequently)
return fetch('https://api.example.com/admin/content').then(r => r.json())
}
// Cache public content
return getCachedPublicContent()
}
'use cache'
async function getCachedPublicContent() {
return fetch('https://api.example.com/public/content').then(r => r.json())
}
// ============================================================================
// Example 8: Inline "use cache" (Granular Control)
// ============================================================================
export async function MixedCachingComponent() {
// This function call is cached
const cachedData = await (async function() {
'use cache'
return fetch('https://api.example.com/slow-data').then(r => r.json())
})()
// This function call is NOT cached
const freshData = await fetch('https://api.example.com/fresh-data').then(r => r.json())
return (
<div>
<div>Cached: {cachedData.value}</div>
<div>Fresh: {freshData.value}</div>
</div>
)
}
// ============================================================================
// Migration Guide: Next.js 15 → Next.js 16
// ============================================================================
// ❌ BEFORE (Next.js 15 - Implicit Caching):
/*
// All Server Components were cached by default
export async function MyComponent() {
const data = await fetch('https://api.example.com/data')
return <div>{data.value}</div>
}
// To opt-out of caching:
export const revalidate = 0 // or export const dynamic = 'force-dynamic'
*/
// ✅ AFTER (Next.js 16 - Explicit Opt-In Caching):
/*
// Components are NOT cached by default
export async function MyComponent() {
const data = await fetch('https://api.example.com/data')
return <div>{data.value}</div>
}
// To opt-IN to caching, add "use cache"
'use cache'
export async function MyCachedComponent() {
const data = await fetch('https://api.example.com/data')
return <div>{data.value}</div>
}
*/
// ============================================================================
// Cache Behavior Summary
// ============================================================================
/**
* "use cache" can be added to:
* 1. ✅ Components (entire component cached)
* 2. ✅ Functions (function output cached)
* 3. ✅ Pages (entire page cached)
* 4. ✅ Layouts (layout cached)
* 5. ✅ Inline async functions (granular caching)
*
* Default behavior (without "use cache"):
* - Server Components: NOT cached (change from Next.js 15)
* - fetch() calls: Cached by default (unchanged)
*
* Revalidation:
* - Use revalidateTag() to invalidate cache by tag
* - Use updateTag() for immediate read-your-writes
* - Use refresh() for uncached data only
*
* When to use "use cache":
* ✅ Expensive computations (database queries, API calls)
* ✅ Stable data (product catalogs, blog posts)
* ✅ Partial Prerendering (static header + dynamic user info)
*
* When NOT to use "use cache":
* ❌ Real-time data (metrics, notifications)
* ❌ User-specific data (unless using cookies/headers for cache keys)
* ❌ Frequently changing data (stock prices, live scores)
*/
{
"name": "nextjs-16-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit"
},
"dependencies": {
"next": "^16.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0"
},
"optionalDependencies": {
"zod": "^3.24.0",
"@tailwindcss/vite": "^4.1.0",
"tailwindcss": "^4.1.0"
},
"engines": {
"node": ">=20.9.0",
"npm": ">=10.0.0"
}
}
/**
* Next.js 16 - Parallel Routes with Required default.js
*
* BREAKING CHANGE: Parallel routes now REQUIRE explicit default.js files.
* Without them, routes will fail during soft navigation.
*
* Directory 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
*/
// ============================================================================
// Example 1: Modal + Main Content (Common Pattern)
// ============================================================================
// File: app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
{modal}
<main>{children}</main>
</body>
</html>
)
}
// File: app/@modal/login/page.tsx
export default function LoginModal() {
return (
<div className="modal-overlay">
<div className="modal">
<h2>Login</h2>
<form>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
</div>
</div>
)
}
// File: app/@modal/default.tsx (REQUIRED)
export default function ModalDefault() {
return null // No modal shown by default
}
// File: app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Home Page</h1>
<a href="/login">Open Login Modal</a>
</div>
)
}
// ============================================================================
// Example 2: Dashboard with Multiple Panels
// ============================================================================
// File: app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
activity,
}: {
children: React.ReactNode
analytics: React.ReactNode
notifications: React.ReactNode
activity: React.ReactNode
}) {
return (
<div className="dashboard-layout">
<aside className="sidebar">
{notifications}
</aside>
<main className="main-content">
{children}
{analytics}
</main>
<aside className="activity-sidebar">
{activity}
</aside>
</div>
)
}
// File: app/dashboard/@analytics/overview/page.tsx
export default async function AnalyticsOverview() {
const stats = await fetch('https://api.example.com/stats').then(r => r.json())
return (
<div className="analytics-panel">
<h2>Analytics</h2>
<div>
<p>Page Views: {stats.pageViews}</p>
<p>Unique Visitors: {stats.uniqueVisitors}</p>
</div>
</div>
)
}
// File: app/dashboard/@analytics/default.tsx (REQUIRED)
export default function AnalyticsDefault() {
return (
<div className="analytics-panel">
<h2>Analytics</h2>
<p>No analytics data available</p>
</div>
)
}
// File: app/dashboard/@notifications/default.tsx (REQUIRED)
export default function NotificationsDefault() {
return (
<div className="notifications-panel">
<h3>Notifications</h3>
<p>No new notifications</p>
</div>
)
}
// File: app/dashboard/@activity/default.tsx (REQUIRED)
export default function ActivityDefault() {
return (
<div className="activity-panel">
<h3>Recent Activity</h3>
<p>No recent activity</p>
</div>
)
}
// ============================================================================
// Example 3: E-commerce with Product + Reviews
// ============================================================================
// File: app/products/[id]/layout.tsx
export default function ProductLayout({
children,
reviews,
recommendations,
}: {
children: React.ReactNode
reviews: React.ReactNode
recommendations: React.ReactNode
}) {
return (
<div className="product-layout">
<div className="product-main">
{children}
</div>
<div className="product-sidebar">
{reviews}
{recommendations}
</div>
</div>
)
}
// File: app/products/[id]/@reviews/page.tsx
export default async function ProductReviews({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const reviews = await fetch(`https://api.example.com/products/${id}/reviews`)
.then(r => r.json())
return (
<div className="reviews">
<h3>Reviews</h3>
<ul>
{reviews.map((review: { id: string; rating: number; comment: string }) => (
<li key={review.id}>
<p>⭐ {review.rating}/5</p>
<p>{review.comment}</p>
</li>
))}
</ul>
</div>
)
}
// File: app/products/[id]/@reviews/default.tsx (REQUIRED)
export default function ReviewsDefault() {
return (
<div className="reviews">
<h3>Reviews</h3>
<p>No reviews yet</p>
</div>
)
}
// File: app/products/[id]/@recommendations/default.tsx (REQUIRED)
export default function RecommendationsDefault() {
return (
<div className="recommendations">
<h3>Recommendations</h3>
<p>Loading recommendations...</p>
</div>
)
}
// ============================================================================
// Example 4: Auth-Gated Content
// ============================================================================
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
// File: app/@auth/default.tsx (REQUIRED)
export default async function AuthDefault() {
const cookieStore = await cookies()
const isAuthenticated = cookieStore.get('auth')?.value
if (!isAuthenticated) {
redirect('/login')
}
return null
}
// File: app/@auth/profile/page.tsx
export default async function ProfilePage() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const user = await fetch(`https://api.example.com/users/${userId}`)
.then(r => r.json())
return (
<div>
<h2>Profile</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
)
}
// ============================================================================
// Example 5: Conditional Rendering Based on Slot
// ============================================================================
// File: app/layout.tsx
export default function Layout({
children,
banner,
}: {
children: React.ReactNode
banner: React.ReactNode
}) {
// Only show banner on specific pages
const showBanner = true // Determine based on route
return (
<html>
<body>
{showBanner && banner}
<main>{children}</main>
</body>
</html>
)
}
// File: app/@banner/sale/page.tsx
export default function SaleBanner() {
return (
<div className="banner sale-banner">
🎉 50% OFF SALE! Use code SALE50
</div>
)
}
// File: app/@banner/default.tsx (REQUIRED)
export default function BannerDefault() {
return null // No banner by default
}
// ============================================================================
// Example 6: Loading States with Parallel Routes
// ============================================================================
// File: app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return (
<div className="analytics-panel">
<h2>Analytics</h2>
<p>Loading analytics...</p>
<div className="skeleton-loader" />
</div>
)
}
// File: app/dashboard/@notifications/loading.tsx
export default function NotificationsLoading() {
return (
<div className="notifications-panel">
<h3>Notifications</h3>
<div className="skeleton-loader" />
</div>
)
}
// ============================================================================
// Example 7: Error Boundaries with Parallel Routes
// ============================================================================
// File: app/dashboard/@analytics/error.tsx
'use client'
export default function AnalyticsError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="analytics-panel error">
<h2>Analytics</h2>
<p>Failed to load analytics</p>
<button onClick={reset}>Try Again</button>
</div>
)
}
// ============================================================================
// Migration Guide: Next.js 15 → Next.js 16
// ============================================================================
/**
* BREAKING CHANGE: default.js is now REQUIRED for all parallel routes
*
* ❌ BEFORE (Next.js 15):
* app/
* ├── @modal/
* │ └── login/
* │ └── page.tsx
* └── layout.tsx
*
* This worked in Next.js 15. If no matching route, Next.js rendered nothing.
*
* ✅ AFTER (Next.js 16):
* app/
* ├── @modal/
* │ ├── login/
* │ │ └── page.tsx
* │ └── default.tsx ← REQUIRED! Will error without this
* └── layout.tsx
*
* Why the change?
* Next.js 16 changed how parallel routes handle soft navigation. Without
* default.js, unmatched slots will error during client-side navigation.
*
* What should default.tsx return?
* - return null (most common - no UI shown)
* - return <Skeleton /> (loading placeholder)
* - redirect() to another route
* - return fallback UI
*/
// ============================================================================
// Common Patterns for default.tsx
// ============================================================================
// Pattern 1: Null (no UI)
export function DefaultNull() {
return null
}
// Pattern 2: Loading skeleton
export function DefaultSkeleton() {
return (
<div className="skeleton">
<div className="skeleton-line" />
<div className="skeleton-line" />
<div className="skeleton-line" />
</div>
)
}
// Pattern 3: Fallback message
export function DefaultFallback() {
return (
<div>
<p>Content not available</p>
</div>
)
}
// Pattern 4: Redirect
import { redirect } from 'next/navigation'
export function DefaultRedirect() {
redirect('/dashboard')
}
/**
* Summary:
*
* Parallel Routes in Next.js 16:
* 1. ✅ Use @folder convention for parallel slots
* 2. ✅ MUST include default.tsx for each @folder
* 3. ✅ default.tsx handles unmatched routes during navigation
* 4. ✅ Can have loading.tsx for loading states
* 5. ✅ Can have error.tsx for error boundaries
*
* Common use cases:
* - Modals + main content
* - Dashboard panels
* - Product + reviews/recommendations
* - Conditional banners
* - Auth-gated content
*
* Best practices:
* - Keep default.tsx simple (usually return null)
* - Use loading.tsx for better UX
* - Use error.tsx for error handling
* - Test soft navigation (client-side routing)
*/
/**
* Next.js 16 - Proxy Migration (middleware.ts → proxy.ts)
*
* BREAKING CHANGE: middleware.ts is deprecated in Next.js 16.
* Use proxy.ts instead.
*
* Migration: Rename file and function, keep same logic.
*/
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// ============================================================================
// Example 1: Basic Proxy (Auth Check)
// ============================================================================
export function proxy(request: NextRequest) {
const token = request.cookies.get('token')
// Redirect to login if no token
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}
// ============================================================================
// Example 2: Advanced Proxy (Multiple Checks)
// ============================================================================
export function advancedProxy(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. Auth check
const token = request.cookies.get('token')
if (pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 2. Role-based access
const userRole = request.cookies.get('role')?.value
if (pathname.startsWith('/admin') && userRole !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
// 3. Add custom headers
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
response.headers.set('x-pathname', pathname)
return response
}
export const advancedConfig = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
// ============================================================================
// Example 3: Request Rewriting
// ============================================================================
export function rewriteProxy(request: NextRequest) {
// Rewrite /blog/* to /posts/*
if (request.nextUrl.pathname.startsWith('/blog')) {
const url = request.nextUrl.clone()
url.pathname = url.pathname.replace('/blog', '/posts')
return NextResponse.rewrite(url)
}
return NextResponse.next()
}
export const rewriteConfig = {
matcher: '/blog/:path*',
}
// ============================================================================
// Example 4: Geolocation-Based Routing
// ============================================================================
export function geoProxy(request: NextRequest) {
const country = request.geo?.country || 'US'
const url = request.nextUrl.clone()
// Redirect to country-specific page
if (url.pathname === '/') {
url.pathname = `/${country.toLowerCase()}`
return NextResponse.rewrite(url)
}
return NextResponse.next()
}
// ============================================================================
// Example 5: A/B Testing
// ============================================================================
export function abTestProxy(request: NextRequest) {
const bucket = request.cookies.get('bucket')?.value
if (!bucket) {
// Assign to A or B randomly
const newBucket = Math.random() < 0.5 ? 'a' : 'b'
const response = NextResponse.next()
response.cookies.set('bucket', newBucket, {
maxAge: 60 * 60 * 24 * 30, // 30 days
})
// Rewrite to variant page
if (newBucket === 'b') {
const url = request.nextUrl.clone()
url.pathname = `/variant-b${url.pathname}`
return NextResponse.rewrite(url)
}
return response
}
// Existing user
if (bucket === 'b') {
const url = request.nextUrl.clone()
url.pathname = `/variant-b${url.pathname}`
return NextResponse.rewrite(url)
}
return NextResponse.next()
}
export const abTestConfig = {
matcher: '/',
}
// ============================================================================
// Example 6: Rate Limiting
// ============================================================================
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
export function rateLimitProxy(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const now = Date.now()
// Check rate limit (100 requests per minute)
const rateLimit = rateLimitMap.get(ip)
if (rateLimit) {
if (now < rateLimit.resetAt) {
if (rateLimit.count >= 100) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': String(Math.ceil((rateLimit.resetAt - now) / 1000)),
},
})
}
rateLimit.count++
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 }) // 1 minute
}
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
}
return NextResponse.next()
}
export const rateLimitConfig = {
matcher: '/api/:path*',
}
// ============================================================================
// Example 7: Response Modification
// ============================================================================
export function modifyResponseProxy(request: NextRequest) {
const response = NextResponse.next()
// Add security headers
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'origin-when-cross-origin')
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
)
return response
}
// ============================================================================
// Migration Guide: middleware.ts → proxy.ts
// ============================================================================
/**
* ❌ BEFORE (Next.js 15):
*
* // File: middleware.ts
* import { NextResponse } from 'next/server'
* import type { NextRequest } from 'next/server'
*
* export function middleware(request: NextRequest) {
* 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 (Next.js 16):
*
* // File: proxy.ts
* import { NextResponse } from 'next/server'
* import type { NextRequest } from 'next/server'
*
* export function proxy(request: NextRequest) {
* 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*',
* }
*/
/**
* Migration Steps:
* 1. Rename file: middleware.ts → proxy.ts
* 2. Rename function: middleware → proxy
* 3. Keep config object the same
* 4. Logic remains identical
*
* Why the change?
* - proxy.ts runs on Node.js runtime (full Node.js APIs)
* - middleware.ts ran on Edge runtime (limited APIs)
* - proxy.ts makes the network boundary explicit
*
* Note: middleware.ts still works in Next.js 16 but is deprecated.
* Migrate to proxy.ts for future compatibility.
*/
/**
* Summary:
*
* Proxy patterns:
* 1. ✅ Auth checks and redirects
* 2. ✅ Role-based access control
* 3. ✅ Custom headers
* 4. ✅ Request rewriting (URL rewrites)
* 5. ✅ Geolocation-based routing
* 6. ✅ A/B testing
* 7. ✅ Rate limiting
* 8. ✅ Response modification (security headers)
*
* Best practices:
* - Keep proxy logic lightweight (runs on every request)
* - Use matcher to limit scope
* - Avoid database queries (use cookies/headers instead)
* - Cache rate limit data in memory (or Redis for production)
* - Return NextResponse.next() if no action needed
*/
/**
* Next.js 16 - Route Handlers (API Endpoints)
*
* Route Handlers replace API Routes from Pages Router.
* File: app/api/[...]/route.ts
*/
import { NextResponse } from 'next/server'
import { cookies, headers } from 'next/headers'
// ============================================================================
// Example 1: Basic CRUD API
// ============================================================================
// GET /api/posts
export async function GET() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return NextResponse.json(posts)
}
// POST /api/posts
export async function POST(request: Request) {
const body = await request.json()
const post = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(post, { status: 201 })
}
// ============================================================================
// Example 2: Dynamic Routes
// ============================================================================
// File: app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params // ✅ Await params in Next.js 16
const post = await fetch(`https://api.example.com/posts/${id}`)
.then(r => r.json())
.catch(() => null)
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const body = await request.json()
const updated = await fetch(`https://api.example.com/posts/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(updated)
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
await fetch(`https://api.example.com/posts/${id}`, {
method: 'DELETE',
})
return NextResponse.json({ message: 'Post deleted' }, { status: 200 })
}
// ============================================================================
// Example 3: Search with Query Parameters
// ============================================================================
// GET /api/search?q=nextjs&limit=10&page=1
export async function SEARCH(request: Request) {
const { searchParams } = new URL(request.url)
const query = searchParams.get('q') || ''
const limit = parseInt(searchParams.get('limit') || '10')
const page = parseInt(searchParams.get('page') || '1')
const offset = (page - 1) * limit
const results = await fetch(
`https://api.example.com/search?q=${query}&limit=${limit}&offset=${offset}`
).then(r => r.json())
return NextResponse.json({
results: results.items,
total: results.total,
page,
limit,
})
}
// ============================================================================
// Example 4: Authentication with Cookies
// ============================================================================
// POST /api/auth/login
export async function LOGIN(request: Request) {
const { email, password } = await request.json()
// Verify credentials
const user = await fetch('https://api.example.com/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
}).then(r => r.json())
if (!user.token) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}
// Set cookie
const response = NextResponse.json({ success: true })
response.cookies.set('token', user.token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 7 days
})
return response
}
// GET /api/auth/me
export async function ME() {
const cookieStore = await cookies() // ✅ Await cookies in Next.js 16
const token = cookieStore.get('token')?.value
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const user = await fetch('https://api.example.com/auth/me', {
headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json())
return NextResponse.json(user)
}
// ============================================================================
// Example 5: Webhook Handler
// ============================================================================
// File: app/api/webhooks/stripe/route.ts
import { headers as getHeaders } from 'next/headers'
export async function WEBHOOK(request: Request) {
const body = await request.text()
const headersList = await getHeaders() // ✅ Await headers in Next.js 16
const signature = headersList.get('stripe-signature')
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 400 }
)
}
// Verify webhook signature (example with Stripe)
let event
try {
event = JSON.parse(body)
// In production: stripe.webhooks.constructEvent(body, signature, secret)
} catch (err) {
return NextResponse.json(
{ error: 'Invalid payload' },
{ status: 400 }
)
}
// 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
default:
console.log(`Unhandled event type: ${event.type}`)
}
return NextResponse.json({ received: true })
}
async function handlePaymentSuccess(paymentIntent: any) {
console.log('Payment succeeded:', paymentIntent.id)
// Update database, send confirmation email, etc.
}
async function handlePaymentFailure(paymentIntent: any) {
console.log('Payment failed:', paymentIntent.id)
// Notify user, log error, etc.
}
// ============================================================================
// Example 6: Streaming Response
// ============================================================================
// GET /api/stream
export async function STREAM() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
const data = `data: ${JSON.stringify({ count: i })}\n\n`
controller.enqueue(encoder.encode(data))
await new Promise(resolve => setTimeout(resolve, 1000))
}
controller.close()
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}
// ============================================================================
// Example 7: File Upload
// ============================================================================
// POST /api/upload
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function UPLOAD(request: Request) {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
)
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filename = `${Date.now()}-${file.name}`
const path = join(process.cwd(), 'public', 'uploads', filename)
await writeFile(path, buffer)
return NextResponse.json({
success: true,
url: `/uploads/${filename}`,
})
}
// ============================================================================
// Example 8: CORS Configuration
// ============================================================================
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
export async function CORS_GET() {
const response = NextResponse.json({ message: 'Hello' })
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
return response
}
// ============================================================================
// Example 9: Error Handling
// ============================================================================
export async function ERROR_HANDLING() {
try {
const data = await fetch('https://api.example.com/data')
.then(r => {
if (!r.ok) throw new Error('API request failed')
return r.json()
})
return NextResponse.json(data)
} catch (error) {
console.error('Error:', error)
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
}
}
// ============================================================================
// Example 10: Rate Limiting
// ============================================================================
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
export async function RATE_LIMITED() {
const headersList = await headers()
const ip = headersList.get('x-forwarded-for') || 'unknown'
const now = Date.now()
const rateLimit = rateLimitMap.get(ip)
if (rateLimit) {
if (now < rateLimit.resetAt) {
if (rateLimit.count >= 10) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
rateLimit.count++
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
}
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
}
return NextResponse.json({ message: 'Success' })
}
/**
* Summary:
*
* Route Handlers (app/api/*/route.ts):
* 1. ✅ Support all HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
* 2. ✅ Await params in Next.js 16
* 3. ✅ Access cookies with await cookies()
* 4. ✅ Access headers with await headers()
* 5. ✅ Use NextResponse.json() for JSON responses
* 6. ✅ Return Response or NextResponse
*
* Common patterns:
* - CRUD operations (GET, POST, PATCH, DELETE)
* - Query parameters with searchParams
* - Authentication with cookies
* - Webhooks with signature verification
* - Streaming responses (SSE, WebSocket)
* - File uploads with FormData
* - CORS configuration
* - Error handling
* - Rate limiting
*
* Best practices:
* - Use try/catch for error handling
* - Return appropriate HTTP status codes
* - Validate input data
* - Set secure cookie options in production
* - Add rate limiting for public endpoints
* - Use CORS headers when needed
*/