Cheat sheet · Next.js App Router From Zero to Production
Next.js App Router cheat sheet
The Next.js 16 App Router on one page: routing, Server and Client Components, caching, Server Actions, route handlers, auth, rendering modes, SEO, config and deployment. Check every tutorial's version: 16 changed several APIs.
blogs · jp-blogs.vercel.app/cheatsheets/series/nextjs
Routing and special filesLesson 0 →
- Each folder in
app/is a URL segment. It is public only with apage(orroute) file.(group)adds no URL part._privateis skipped. [slug]one segment.[...parts]one or more.[[...rest]]zero or more.paramsandsearchParamsare Promises:const { slug } = await props.params.generateStaticParamsbuilds known values (●).dynamicParams = falsemakes the rest 404.- Use the Link component for internal links (no reload, prefetch, layout state kept). A plain anchor reloads the page.
- layout.tsx
- Wraps pages, stays mounted, keeps state
- template.tsx
- Like layout, new instance per navigation
- loading.tsx
- Suspense fallback for the whole page
- error.tsx
- Error boundary: must be
'use client' - not-found.tsx
- 404 screen, also for
notFound() - route.ts
- HTTP endpoint. Never beside
page.tsx - proxy.ts
- Runs before requests (was
middleware.ts)
Server and Client ComponentsLesson 1 →
| Aspect | Server Component | Client Component |
|---|---|---|
| Default | Yes | Needs 'use client' |
| Can use | async, DB, secrets | State, effects, handlers, window |
| Code sent to browser | No | Yes |
| Rendered on server | Yes | Yes, then hydrated |
'use client'marks a boundary: the file and its imports ship. Keep it on small interactive leaves, never on a page or layout.- A Server Component goes inside a Client one only as
childrenor a prop, never by import. - Props crossing the boundary: plain data,
Date,Map,Set,bigintwork. Functions and class instances give a 500. - Stream a promise: start it on the server, pass it down, read it with
useinside Suspense.
Watch out: Props are public (the RSC payload is in the page). Never pass a whole DB row: send a DTO. Add import 'server-only' to server files.
Fetching and request memoizationLesson 2 →
- Fetch in an
asyncServer Component. No effect, no loading state, secrets stay on the server. - Identical
fetchcalls in one render run once (memoization, one render only). Wrap DB reads in Reactcache(). - Independent requests:
Promise.allor one Suspense box each. Sequential awaits took 1.5 s, parallel 0.5 s. - Do not fetch your own route handler from a Server Component. Share a function.
| Default model | Result |
|---|---|
fetch(url), no request data | Prerendered at build, then frozen |
cache: 'no-store' | Dynamic (ƒ) |
next: { revalidate: 60 } | Stale-while-revalidate |
cookies() / headers() / searchParams | Dynamic |
Cache Components (use cache)Lesson 2 →
// next.config.ts
const nextConfig = {
cacheComponents: true };
async function getPosts() {
"use cache";
cacheLife("hours");
cacheTag("posts");
return db.posts.findMany();
}Everything is dynamic until you cache it. Arguments form the cache key.
| cacheLife | Meaning |
|---|---|
stale | Browser reuses its copy without asking (default 5 min) |
revalidate | Server refreshes in background after this |
expire | After this, next request waits for fresh data |
- Uncached or runtime data outside Suspense fails the build: stream it, cache it, or block on purpose.
cookies()insideuse cachefails: read outside, pass as an argument.Date.now()needsconnection()or caching.dynamicandrevalidateexports are build errors whencacheComponentsis on.
Revalidation and cache layersLesson 2 →
| Function | Behaviour | Where |
|---|---|---|
revalidateTag(tag, 'max') | Stale-while-revalidate | Actions, handlers |
updateTag(tag) | Expire now, read your writes | Server Actions only |
revalidatePath(p) | Invalidate a route | Actions, handlers |
refresh() | Re-render current route, cache untouched | Server Actions |
- Layers: request memoization (one render),
use cache(server), prerendered output (build or CDN), router cache (browser, 5 min). - One-argument
revalidateTag('x')is deprecated. Test caching withbuild+start, neverdev. - Default
use cachestore is in memory: serverless instances do not share it.
Server ActionsLesson 3 →
"use server";
export async function addTodo(_p, fd) {
const u = await verifySession();
const r = Schema.safeParse(
{ t: fd.get("t") });
if (!r.success)
return { errors: r.error.flatten() };
await db.todo.create(
{ ...r.data, owner: u.id });
updateTag("todos"); // before redirect
return { ok: true };
}Use with useActionState. Return expected errors, throw unexpected ones.
- Works with JavaScript off (plain POST). With JS it is one fetch round trip that also returns the fresh page.
- An action is a public POST endpoint: authenticate, authorize the row (IDOR), validate, in every action. Hidden button is not security.
- Next.js checks
Originvs host, caps the body at 1 MB, hides action ids.curlskips the origin check. redirect()throws: never insidetry/catch. Revalidate before it. Actions run one at a time.- Extra argument:
action.bind(null, id). Treat it as client input.
Route HandlersLesson 4 →
route.tsexportsGET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS. A missing method gives405.- In the default model
GEThandlers are dynamic. Cache withdynamic = 'force-static'orrevalidate. OnlyGETcaches. - Validate query, body (
400bad JSON,422bad fields), headers. Use201,204,404. Keep one error shape. - Webhooks: HMAC the raw body, compare with
timingSafeEqual, then parse. - CORS is a browser rule, not a firewall. Allow-list the origin, send
Vary: Origin. - Auth, validation and rate limiting inside every handler.
| You need… | Use |
|---|---|
| Render a page's data | Server Component |
| Mutation from your UI | Server Action |
| Public URL (mobile, webhook, partner) | Route Handler |
| Redirect or rewrite by cookie or header | Proxy |
| Fixed redirects, rewrites, headers | next.config.ts |
ProxyLesson 4 →
proxy.ts(renamed frommiddleware.ts) can redirect, rewrite, set headers. One file per project.- Always set a
matcher, or it also runs on_next/static, images andpublic/. NextResponse.next({ request: { headers } })changes what the app receives.response.headers.setchanges what the client gets.- Not for slow work, DB calls or
fetchcaching. Config redirects run before Proxy.
Watch out: Proxy is an optimistic filter, not a lock. A client can forge any header Proxy does not overwrite (an excluded route let x-user-hint: admin through).
Auth: three jobs, five placesLesson 5 →
- Authentication proves who. Session remembers it (cookie). Authorization decides what they may do.
- Session cookie set on the server:
httpOnly,secure,sameSite, expiry.await cookies(). A signed token is readable, so keep secrets out. - Pin the algorithm (
algorithms: ['HS256']) when verifying. Bad token givesnull. - Hash passwords with a slow hash (bcrypt). Give one vague login error.
| Place | Verdict |
|---|---|
Data Access Layer (server-only, cache()) | The real check, near the data, return DTOs |
proxy.ts | Quick redirect only |
layout.tsx | Skipped on client navigation |
| Server Action, Route Handler | Each needs its own check |
Tip: Stateless cookie: cannot revoke early. Database session: instant logout and role change. Wrong owner: 403, or 404 to hide it exists.
Static, ISR, streaming, PPRLesson 6 →
| Symbol | Meaning |
|---|---|
○ Static | Built once, s-maxage=31536000 |
● SSG | Static with generateStaticParams |
ƒ Dynamic | Per request, private, no-store |
◐ Partial Prerender | Static shell plus streamed parts (Cache Components) |
- Static pages are frozen:
new Date()shows build time.revalidate = 5means stale after 5 s, next visit rebuilds (x-nextjs-cache: HIT, STALE, MISS). - Streaming: shell first, each Suspense box when ready, siblings in parallel. Prefer small boundaries. Do not
awaitrequest data at the top of a layout. - Status code is fixed after the first chunk:
notFound()inside Suspense gives200plusnoindex. Decide 404 and redirects before the boundary.
Metadata, SEO, images, fontsLesson 7 →
- Export
metadataorgenerateMetadata(Server Component).viewportis separate. SetmetadataBaseonce, or social images point at localhost. - Metadata merges shallowly: a page's
openGraphreplaces the layout's. Share data withcache(). - Bots wait for the title in the head: keep
generateMetadatafast. opengraph-image.tsx,sitemap.ts,robots.tsmake real files.Disallowis notnoindexand not security.next/image: setwidth/height(no shift), AVIF/WebP,remotePatterns.priorityis deprecated: usepreloadorfetchPriority.next/fontself-hosts and adds a size-matched fallback.
Env vars, config, deploymentLesson 8 →
- Env load order: real environment,
.env.$(NODE_ENV).local,.env.local,.env.$(NODE_ENV),.env. - Server variables stay on the server.
NEXT_PUBLIC_values are inlined at build time;docker run -ecannot change them. - Static pages read env at build. Dynamic routes read it per request.
output: 'standalone': runnode server.js, copypublicand.next/staticyourself (402 MB to 67 MB).- Docker: multi-stage, non-root user,
HOSTNAME=0.0.0.0,ARGfor public values. instrumentation.ts:register()once,onRequestErrorfor errors. Add a cheap dynamic health route.
Watch out: Several instances need a shared cache handler, the same NEXT_SERVER_ACTIONS_ENCRYPTION_KEY and a deployment ID. Proxies that buffer break streaming (X-Accel-Buffering: no).
Interview one-linersLesson 9 →
| Question | Short answer |
|---|---|
Does 'use client' mean browser only? | No. Also rendered on the server, then hydrated. |
Is fetch cached in 16? | No, but a page with no request data is frozen at build. |
| Layout auth check enough? | No. Layouts skip on client navigation. |
| Server Action secure if button hidden? | No. Public POST endpoint. |
| Why old price in prod, fine in dev? | Static page frozen at build. Dev renders on demand. |
redirect() in try/catch? | It throws; the catch swallows it. |
NEXT_PUBLIC_ wrong in prod? | Frozen at build time, not run time. |
getServerSideProps replacement? | Server Component on a dynamic route. |
A cheat sheet is a reminder, not a teacher. Each card links to the lesson that explains it step by step.