Next.js 16 App Router Best Practices
Comprehensive Next.js 16 App Router guide for AI agents. Contains 45 rules across 9 categories, prioritized by impact from critical (build optimization, caching strategy) through to cross-cutting codebase hygiene (dedup, dead routes, boundary coherence). Reflects Next.js 16 changes: 'use cache' directive replacing implicit caching, revalidateTag(tag, cacheLife) requirement, proxy.ts replacing middleware.ts, Turbopack persistent caching, App Router conventions.
Rule files describe pattern shapes (not API names) and open with a "Shapes to recognize" section listing 2–4 syntactic disguises the same break can wear. Selected high-value rules (those whose disguises are most common in practice — 'use cache', parallel-fetching, dynamic-imports, server-action-forms, client-boundary, server-vs-client-fetching) include an extra concrete "In disguise" incorrect/correct example pair to teach pattern detection beyond the grep-friendly cases.
When to Apply
- Writing new Next.js 16 App Router code
- Auditing or modernizing a Next.js codebase — single file, PR, or whole repo (see
references/_review-algorithm.md) - Migrating from Next.js 15 to 16 (implicit caching →
'use cache',middleware.ts→proxy.ts,revalidateTagsingle-arg → withcacheLife) - Configuring caching strategies with
'use cache',unstable_cache,revalidateTag,revalidatePath - Implementing Server Components and parallel/colocated data fetching
- Setting up parallel routes, intercepting routes, prefetching,
proxy.ts - Creating Server Actions for form handling and mutations
- Tuning
'use client'boundaries to minimize client bundle - Finding codebase-level issues that single-file rules can't see: duplicated server fetchers, near-duplicate routes/layouts, dead routes,
'use client'propagation across the route tree, prop-shape drift (see Category 9)
How to Review or Refactor a Codebase
When the user asks to review, refactor, modernize, or audit Next.js code — single file or whole repo — follow references/_review-algorithm.md. Do not improvise.
Four non-negotiables from that doc:
- Two modes — never refuse a whole-repo audit. Pick Mode A (scoped, ≤~20 files) or Mode B (whole-tree: inventory pass + targeted sweeps + full Category 9).
- Judgment over grep. Each rule names a pattern shape, not a syntactic marker. Read each rule's Shapes to recognize section before sweeping — grep finds the easy violations and misses the high-value ones (a layout marked
'use client'for one button; TanStack Query fetching initial page data; a route handler doing the work of a Server Action; a hand-rolled cache layer mimicking'use cache'; sequential fetches hidden across parent/child Server Components). - Category-major, not file-major — with forcing functions. Sweep one category at a time across all in-scope files in priority order (CRITICAL → … → CROSS-CUTTING). The algorithm requires a scope declaration, per-category progress lines, and a final coverage table (category × file/bucket, cells ∈
{clean, N findings, n/a}). A missing category in the output is immediately visible. - Codebase-level findings come from Category 9. Single-file rules can't tell you "these two routes should be one" or "this server action is dead." Category 9 (Codebase Hygiene) sweeps the full inventory at the end and produces remove / dedup / reuse / consolidate findings.
Single-file ad-hoc questions ("is this caching strategy right?") can go straight to the relevant rule. The algorithm exists for the multi-file and whole-repo cases.
Rule Categories
| # | Category | Impact | Rules | Key Topics |
|---|----------|--------|-------|------------|
| 1 | Build & Bundle Optimization | CRITICAL | 5 | Turbopack, optimizePackageImports, dynamic imports, barrel files, serverExternalPackages |
| 2 | Caching Strategy | CRITICAL | 6 | 'use cache', revalidateTag+cacheLife, fetch options, segment config |
| 3 | Server Components & Data Fetching | HIGH | 6 | Parallel fetching, streaming, colocation, preload, no-client-fetch, error handling |
| 4 | Routing & Navigation | HIGH | 5 | Parallel routes, intercepting routes, prefetching, proxy.ts, notFound() |
| 5 | Server Actions & Mutations | MEDIUM-HIGH | 5 | Server actions, useFormStatus, action-result errors, useOptimistic, revalidation |
| 6 | Streaming & Loading States | MEDIUM | 5 | Suspense placement, loading.tsx, error.tsx, skeleton matching, nested Suspense |
| 7 | Metadata & SEO | MEDIUM | 4 | generateMetadata, sitemap.ts, robots.ts, opengraph-image.tsx |
| 8 | Client Components | LOW-MEDIUM | 4 | 'use client' boundary, children pattern, hydration mismatch, next/script |
| 9 | Codebase Hygiene | LOW-MEDIUM | 5 | Dedup server fetchers, route consolidation, dead routes/actions, 'use client' propagation, prop drift |
Quick Reference
Critical patterns — get these right first:
- Add
'use cache'to Server Components/functions whose results should be cached (Next.js 16 dropped implicit fetch caching) - Call
revalidateTag(tag, cacheLife)with a profile — never the one-arg API - Configure
optimizePackageImportsfor icon/utility libraries with flat-export surfaces - Don't disable Turbopack persistent caching
- Wrap
<form action={serverAction}>instead of POST-to-/api/...
Next.js 16 idioms (do NOT generate Next.js 15 patterns):
proxy.ts(Node runtime) — notmiddleware.ts(Edge)- Explicit
'use cache'— not implicit fetch caching revalidateTag(tag, cacheLife)— not single-argrevalidateTag(tag)- Server Action +
useActionState— not clientfetch+useState app/sitemap.ts— not hand-maintainedpublic/sitemap.xml
Common single-file mistakes — avoid these anti-patterns:
- Sequential
awaitfor independent data (usePromise.allor preload) useEffect+fetchin a Client Component for initial page data'use client'at the layout level for one interactive button- Missing
revalidatePath/revalidateTagafter a mutating Server Action - Skeletons that don't match content dimensions (CLS hit)
Codebase-level patterns — surface these in Category 9 sweeps:
- 2+ Server Components hitting the same upstream with drifting cache policies — extract to a shared cached fetcher
- 2+ near-duplicate routes/layouts that should be one with a variant or dynamic segment — consolidate
- Routes / route handlers / Server Actions with no inbound traffic for 90+ days — delete (after analytics check)
'use client'propagating up the route tree because of one interactive leaf — demote layouts/parents to Server Components, leave a client island- Same concept under different route-param/search-param/prop names — converge on a canonical name (watch out for SEO redirects)
Table of Contents
- Build & Bundle Optimization — CRITICAL
- 1.1 Import from the source module, not from a barrel
index.ts— CRITICAL (2-10x faster dev startup) - 1.2 Declare package-flat-export libraries in
optimizePackageImports— CRITICAL (200-800ms faster imports, 50-80% smaller bundles) - 1.3 Mark Node packages with native bindings as
serverExternalPackages— HIGH - 1.4 Don't disable Turbopack's persistent caching — CRITICAL (5-10x faster cold starts)
- 1.5 Split heavy components into separately loaded chunks — CRITICAL (30-70% smaller initial bundle)
- 1.1 Import from the source module, not from a barrel
- Caching Strategy — CRITICAL
- 2.1 Make every server
fetchdeclare its caching intent — HIGH - 2.2 Declare route-level caching via segment-config exports — MEDIUM-HIGH
- 2.3 Mark cacheable Server Components/functions explicitly with
'use cache'— CRITICAL - 2.4 Call
revalidateTag(tag, cacheLife)with a profile — CRITICAL - 2.5 Every Server Action that mutates must invalidate the routes/tags that surface it — HIGH
- 2.6 Wrap per-request fetchers with React
cache()for dedup — HIGH
- 2.1 Make every server
- Server Components & Data Fetching — HIGH
- 3.1 Independent server fetches run concurrently — sequential
awaitis a waterfall — HIGH - 3.2 Wrap each independently-paced async leaf in its own
<Suspense>— HIGH - 3.3 Each Server Component fetches the data it renders — HIGH
- 3.4 Trigger critical data fetches at the top via a
preloadcall — MEDIUM-HIGH - 3.5 Initial page data lands in HTML via a Server Component — never
useEffect+fetch— MEDIUM-HIGH - 3.6 Contain async failures via
error.tsxorErrorBoundary— MEDIUM
- 3.1 Independent server fetches run concurrently — sequential
- Routing & Navigation — HIGH
- 4.1 Multi-region layouts use parallel-route slots — HIGH
- 4.2 Modal/lightbox detail views use intercepting routes — HIGH
- 4.3 Tune
<Link prefetch>to traffic likelihood — MEDIUM-HIGH - 4.4 Network-boundary logic lives in
proxy.ts— notmiddleware.ts— MEDIUM-HIGH - 4.5 Missing dynamic resource calls
notFound()for real HTTP 404 — MEDIUM
- Server Actions & Mutations — MEDIUM-HIGH
- 5.1 Mutations from forms run through Server Actions — not API routes + client
fetch— MEDIUM-HIGH - 5.2 Submit buttons read parent-form pending state from
useFormStatus— MEDIUM-HIGH - 5.3 Server Actions return a typed error/state result — never throw silently — MEDIUM-HIGH
- 5.4 Mutations with predictable UI outcomes apply optimistically — MEDIUM
- 5.5 Every Server Action invalidates the routes/tags that surface its data — MEDIUM
- 5.1 Mutations from forms run through Server Actions — not API routes + client
- Streaming & Loading States — MEDIUM
- 6.1 Place Suspense around independently-paced subtrees — MEDIUM
- 6.2 Every route has a
loading.tsxadjacent to itspage.tsx— MEDIUM - 6.3 Every route has an
error.tsxnext to it — MEDIUM - 6.4 Skeletons match the dimensions of the content they replace — MEDIUM
- 6.5 Nest Suspense when content has a natural reveal order — LOW-MEDIUM
- Metadata & SEO — MEDIUM
- 7.1 Dynamic routes export
generateMetadatafor per-resource SEO — MEDIUM - 7.2 Generate sitemaps from actual data — never hand-maintain XML — MEDIUM
- 7.3 Make crawl rules explicit via
app/robots.tsand per-page metadata — MEDIUM - 7.4 Generate per-page OG images via
opengraph-image.tsx— LOW-MEDIUM
- 7.1 Dynamic routes export
- Client Components — LOW-MEDIUM
- 8.1 Push
'use client'down to the interactive leaf — LOW-MEDIUM - 8.2 Server content reaches inside a Client Component via
children— LOW-MEDIUM - 8.3 SSR and client initial render must produce identical HTML — LOW-MEDIUM
- 8.4 Wrap third-party scripts in
next/scriptwith the rightstrategy— LOW-MEDIUM
- 8.1 Push
- Codebase Hygiene — CROSS-CUTTING (multi-file findings; required for whole-repo audits)
- 9.1 Extract duplicated server-side fetchers/actions into a shared module — HIGH
- 9.2 Consolidate near-duplicate routes/layouts/components — HIGH
- 9.3 Delete unreachable routes, unused Server Actions, orphan utilities — MEDIUM-HIGH
- 9.4 Audit
'use client'placement across the route tree — HIGH - 9.5 Converge on canonical names for the same concept across routes/components — MEDIUM-HIGH
References
Related Skills
- For React 19 fundamentals (concurrent rendering, hooks, components), see
reactskill - For client-side form handling, see
react-hook-formskill - For client data caching with TanStack Query, see
tanstack-queryskill
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md