Next.js App Router Patterns
Before you write code
- Read
package.jsonfor thenextversion. In 14,fetchis cached by default; in 15+ it is not, andparams,searchParams,cookies()andheaders()are async and must be awaited. - Read
next.config.*forexperimentalflags (ppr,dynamicIO,useCache,typedRoutes) — they change which caching APIs are correct. - Look at one existing route segment to copy the file conventions already in use (
loading.tsx,error.tsx, colocated_components/).
Route structure
- One folder per URL segment; use route groups
(marketing)/(app)to separate layouts without affecting URLs. - Put private helpers in
_lib/or_components/inside the segment so they are not treated as routes. - Prefer
layout.tsxfor shared chrome andtemplate.tsxonly when you need a fresh instance per navigation. - Add
loading.tsxfor any segment that awaits data anderror.tsx(a Client Component) for any segment that can fail. Addnot-found.tsxat the root and callnotFound()for missing entities. - Dynamic segments:
[slug]; catch-all[...parts]; parallel routes@modalfor modals that must survive refresh; intercepting routes(.)photo/[id]for the "open in modal, deep link to page" pattern.
Server vs Client Components
Default to Server Components. Add 'use client' only at the leaf that needs state, effects, browser APIs or event handlers. Rules:
- Never import a Server Component into a Client Component; pass it as
childrenor a prop instead. - Keep secrets and database clients in server-only modules and guard them with
import 'server-only'. - Props crossing the boundary must be serialisable (no functions, Dates become strings, no class instances).
- Read request data with
await headers()/await cookies()in Server Components or actions, not in client code.
Data fetching and caching
- Fetch in the component that needs the data; React deduplicates identical requests in one render. For non-
fetchsources (ORMs), wrap the function withcache()from React. - Parallelise independent fetches with
Promise.all; use<Suspense>boundaries to stream slow parts. - Explicitly choose a cache policy per request:
fetch(url, { next: { revalidate: 3600, tags: ['posts'] } })orcache: 'no-store'. Do not rely on defaults across versions. - Export
export const revalidate = 60orexport const dynamic = 'force-dynamic'at the segment level only when the whole segment needs it. - Invalidate with
revalidateTag('posts')orrevalidatePath('/posts')inside the Server Action that mutated the data. - With
dynamicIO/use cacheenabled (Next 15.1+/16), use the'use cache'directive andcacheTag/cacheLifeinstead of thenextfetch options.
Mutations with Server Actions
'use server'
import { z } from 'zod'
const Schema = z.object({ title: z.string().min(1) })
export async function createPost(prev: State, formData: FormData): Promise<State> {
const parsed = Schema.safeParse(Object.fromEntries(formData))
if (!parsed.success) return { errors: parsed.error.flatten().fieldErrors }
await db.post.create({ data: parsed.data })
revalidateTag('posts')
redirect('/posts')
}
- Validate every input server-side; treat actions as public HTTP endpoints.
- Check authentication inside the action, not only in middleware.
- Use
useActionStatefor form state anduseOptimisticfor instant feedback. redirect()throws; never wrap it intry/catch.
Metadata, images, fonts
- Export
metadataorgenerateMetadatafrom pages; includetitle.templatein the root layout,openGraph, andalternates.canonical. - Generate OG images with
opengraph-image.tsxandImageResponse. - Load fonts with
next/fontin the root layout; never via<link>. - Use
next/imagewithsizesfor responsive images andpriorityfor the LCP image only.
Middleware and auth
Keep middleware.ts (called proxy.ts in Next 16) thin: redirects, locale detection, coarse auth checks using a lightweight session cookie. Do full authorisation in the data layer.
Checklist before finishing
- No
'use client'in layouts or pages unless unavoidable. - Every awaited data call has a cache decision written next to it.
-
loading.tsxanderror.tsxexist for the new segment. - Server Actions validate input and check auth.
-
next buildpasses with no "dynamic server usage" errors and no type errors. -
npx next lintclean.
Pitfalls
- Reading
searchParamsmakes the route dynamic; isolate that in a small Suspense-wrapped child. useRouterfromnext/routeris the Pages Router; usenext/navigation.- Route Handlers (
route.ts) should not be used for same-app mutations when a Server Action fits; use them for webhooks and third-party callers. - Environment variables reach the client only with the
NEXT_PUBLIC_prefix; everything else is undefined in the browser. generateStaticParamsreturning nothing plusdynamicParams = falseyields 404 for every path.