nextjs-app-router-review
Catches Next.js 13+ App Router regressions that are easy to ship and painful to debug in production. Runs before you open a PR.
What to check
1. Server vs client component boundary
- Every file is a server component unless it opens with
"use client". - A
"use client"file must not transitively importserver-onlymodules (db clients, secrets,fs, build-timeprocess.env). If it does, secrets ship to the browser bundle. "use client"propagates. Any component imported by a client component becomes a client component. Verify you did not accidentally client-ify a heavy server tree by importing it from a client parent.- Hooks (
useState,useEffect,useContext, ...) are illegal in server components. Verify bothnext buildand the actual bundle output.
2. Data fetching cache semantics
- Bare
fetch(url)in App Router is cached (SSG-like) by default. Almost never what dashboards want. - Use
fetch(url, { cache: "no-store" })for per-request freshness, or{ next: { revalidate: N } }for ISR. - Do not rely on
cache: "default". The App Router default differs from the browserfetchdefault. - A route becomes dynamic (per-request) as soon as it reads
cookies(),headers(), or unrestrictedsearchParams. Confirm the intended rendering mode in the build output.
3. params and searchParams are async (Next 15+)
- In Next 15,
paramsandsearchParamsare Promises. Await them or wrap inuse(). Not doing so silently returns Promise objects that coerce to[object Object]at runtime. - If you support both Next 14 and 15, gate the shape check explicitly.
4. Route handlers (app/api/*/route.ts)
- Return
ResponseorNextResponse. Returning a plain object gives a 500 with no obvious error. GEThandlers with no cache-control default to static and cache the response. Setexport const dynamic = "force-dynamic"or explicit cache headers.- Handlers do not auto-parse the request body.
await request.json()yourself and error-handle malformed bodies.
5. Middleware
- Runs on Edge runtime. Node built-ins (
fs, mostcrypto,child_process) are unavailable. - Every middleware invocation is a cold call in production. Do not put heavy logic there.
6. Environment variables
process.env.Xin server components is fine. In client components onlyNEXT_PUBLIC_*are available.- A
process.env.SECRET_KEYreferenced inside a"use client"file evaluates toundefinedand often silently branches to unintended code paths.
Common failure patterns to grep for
"use client"at the top of a file that imports fromdb/,auth/, or readsprocess.env.SECRET_*.fetch(...)without an options object in list, dashboard, or admin pages.params.idorsearchParams.qaccessed synchronously in a Next 15 project.export async function GETwithoutdynamic = "force-dynamic"and no explicit cache headers.
When to invoke
Trigger when the user asks for a Next.js code review, mentions App Router pitfalls, or opens a PR touching app/**/page.tsx, app/**/route.ts, middleware.ts, or next.config.*.