Next.js Patterns Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import "server-only" at the top of any module that touches AUTH_SECRET, database URLs, or signing keys so a Client Component importing it fails the build instead of leaking the value into the browser bundle.httpOnly: true to block JS access, secure: true in production to require HTTPS, and sameSite: "lax" for baseline CSRF protection; sameSite: "none" additionally requires secure: true or the browser drops the cookie entirely.NextRequest/NextResponse from next/server so you get .nextUrl, .cookies, .geo, and static helpers like NextResponse.json() without rebuilding them on top of the Web Request.{ params: Promise<{ ... }> }; destructuring without await params yields a Promise and your lookup silently returns undefined.await request.json() throw, so wrap it and return { error: "Invalid JSON" } with status 400 - or validate with a Zod safeParse for fully typed input.new NextResponse(null, { status: 204 }); NextResponse.json(null, { status: 204 }) sends the string "null" as a body and violates the 204 contract.updateMany({ where: { credits: { gt: 0 } }, data: { credits: { decrement: 1 } } }) or "increment first, then check, rollback on overflow" to close the TOCTOU window between read and write.NEXT_PUBLIC_ prefix is inlined into the client bundle at build time, so reserve it for publishable keys and public URLs - database URLs, API keys, and signing secrets must stay server-only.process.env[varName] is always undefined in client code - only literal references like process.env.NEXT_PUBLIC_APP_URL are inlined.process.env through a Zod schema in lib/env.ts so missing or malformed variables fail fast with a clear message before the first request, and you get a fully typed env object for free.error.tsx needs "use client" at the top; without it the build errors and no boundary is installed for that segment.global-error.tsx replaces the entire document, so it must render <html><body>…</body></html>; it also only activates in production (dev shows the Next.js overlay).{ success: true; data: T } | { success: false; error: string } for expected validation failures, and reserve throw for unexpected errors that should trigger the nearest error.tsx.redirect() (and notFound()) throw a NEXT_REDIRECT sentinel error, so a surrounding try/catch swallows the navigation - place the call after all recoverable logic or rethrow the sentinel.output: "standalone" produces a minimal self-contained server, but .next/static and public/ are not included - copy them into the standalone directory (or front with a CDN/reverse proxy) or static assets 404.127.0.0.1 by default, which is unreachable from outside a container; set ENV HOSTNAME="0.0.0.0" (and ENV PORT=3000) in the Dockerfile so the port mapping works.runtime = "edge" runs in a V8 isolate with no Node built-ins - no fs, path, child_process, or Buffer, and you must use globalThis.crypto; fall back to "nodejs" whenever you need those APIs.openGraph, twitter, and alternates resolve against metadataBase; without metadataBase: new URL("https://myapp.com") your OG images and canonicals ship as broken relative paths.async generateMetadata({ params }) (params is a Promise in Next.js 15+) and extend the parent via the ResolvingMetadata argument instead of duplicating fields.app/sitemap.ts auto-serves /sitemap.xml and app/robots.ts auto-serves /robots.txt; split into multiple sitemap Route Handlers once a site exceeds the 50,000-URL sitemap limit._next, api, and files with extensions (e.g., matcher: ["/((?!_next|api|favicon.ico).*)"]) or it redirects static assets into locale-prefixed paths and breaks the page.generateStaticParams; missing locales silently 404 in production unless dynamicParams is enabled.cookies(), headers(), revalidatePath, and revalidateTag throw outside the Next.js request context, so stub them with vi.mock("next/headers", …) / vi.mock("next/cache", …) before importing the module under test.const jsx = await PostList(); render(jsx) instead of render(<PostList />); also mock with vi.mock() before dynamic import() to ensure the mock wins.Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥