React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

best-practicessummarynextjs-setup

Next.js Setup Best Practices

A condensed summary of the 25 most important best practices drawn from every page in this section.

  1. Generate a Real AUTH_SECRET: Auth.js requires a 32+ character AUTH_SECRET; use npx auth secret so you get a cryptographically strong value, and give dev and prod different secrets so a leaked dev key does not compromise real sessions.
  2. Split auth.config From auth.ts: Keep an edge-safe auth.config.ts (no adapters, no DB drivers) separate from auth.ts where you wire in the database adapter, so middleware can call auth() under the Edge Runtime without pulling in Node-only code.
  3. Augment the Session Type: Declare custom claims on Session and JWT in types/next-auth.d.ts via module augmentation so session.user.role, subscriptionStatus, etc. are typed in every Server Component, action, and middleware.
  4. Pin create-next-app for Reproducibility: CLI defaults drift between versions and the interactive mode silently reuses cached preferences, so pin create-next-app@15 (or whichever major you want) and pass --reset-preferences or explicit flags in CI so scaffolds are deterministic.
  5. Cache fetch Explicitly in Next 15: The default in Next.js 15+ is cache: "auto", not force-cache, so set cache or next: { revalidate: N } on every fetch instead of assuming data is cached — silent per-request fetching is a common regression from Next 14.
  6. Copy static and public Into standalone: output: "standalone" produces a tiny self-contained server but excludes .next/static and public/ — your Dockerfile or deploy script must copy both in, otherwise the app serves 404s for assets.
  7. Set HOSTNAME=0.0.0.0 in Containers: The standalone server binds to 127.0.0.1 by default, so inside a Fargate/EKS/Docker container you must set ENV HOSTNAME=0.0.0.0 (and an explicit PORT) or the load balancer's health check never reaches the app.
  8. Use a Shared ISR Cache Handler: ISR data is cached per-container by default, so multi-container deployments show inconsistent versions until you configure a Redis-backed cacheHandler in next.config with cacheMaxMemorySize: 0.
  9. Prefer pm2 reload Over restart: On EC2, pm2 reload does a graceful rolling restart across cluster workers for zero-downtime deploys; pm2 restart kills all workers and produces a visible outage window.
  10. Let Nginx Serve _next/static: Configure Nginx to serve /_next/static/ and public/ directly with Cache-Control: public, immutable, max-age=31536000 so static assets bypass Node entirely and survive app restarts.
  11. Preserve .next/cache Between Deploys: Deploy scripts should delete .next/server/ and .next/static/ but keep .next/cache/ so ISR and build caches survive; a blunt rm -rf .next wipes them and forces full rebuilds plus cold ISR regeneration.
  12. Use a Singleton Prisma Client: Attach the client to globalThis in development so hot-reload does not open a new connection pool every save — without this, Postgres refuses new connections within minutes and the dev server hangs.
  13. Put DATABASE_URL in .env: The Prisma CLI reads .env, not .env.local, so prisma generate and migrate fail silently or with confusing messages if the URL only lives in .env.local.
  14. revalidatePath After Mutations: Server Actions that write data must call revalidatePath (or revalidateTag) afterward, otherwise the cached Server Component render keeps serving stale data and the UI appears to not update.
  15. Use prisma migrate deploy in CI: CI pipelines should run prisma migrate deploy (non-interactive, applies committed migrations); prisma migrate dev is interactive, can reset the database, and must never run against production or staging.
  16. Validate MDX Frontmatter With Zod: gray-matter returns whatever keys happen to be in the file, so run the result through a Zod schema to catch typos and missing required fields at import time instead of shipping broken docs.
  17. Escape Braces and JSX in MDX Prose: Curly braces evaluate as JavaScript and <Tag /> parses as JSX, so wrap code samples in backticks or escape the braces — otherwise MDX throws build errors on prose that looks fine in plain Markdown.
  18. Create Stripe Customers at Signup: Create the Stripe customer in Auth.js events.createUser, not during checkout, so there is no double-click race that produces duplicate customers and the user has a Stripe ID the moment they exist.
  19. Verify Stripe Webhooks With constructEvent: A Stripe webhook handler must read the raw body and call stripe.webhooks.constructEvent(rawBody, sig, whsec); parsing JSON without verifying the signature lets anyone mutate your database via forged POSTs.
  20. Force nodejs Runtime for Webhooks: Set export const runtime = "nodejs" on the Stripe webhook route because crypto.createHmac is not available on the Edge Runtime, and use a distinct STRIPE_WEBHOOK_SECRET per environment or signatures silently fail.
  21. Tailwind v4 Config Lives in CSS: Tailwind v4 has no tailwind.config.js — theme tokens, variants, and plugins are declared in globals.css via @theme and @custom-variant, and the PostCSS plugin is @tailwindcss/postcss, not the old tailwindcss package.
  22. Always Compose Classes With cn(): shadcn components collapse conflicting utilities via cn() (clsx + tailwind-merge), so concatenate classes through cn(...) instead of template strings — otherwise consumer prop overrides randomly lose to defaults.
  23. Declare env Vars in turbo.json: Turborepo strips any environment variable not listed in the task's env array, so unlisted vars are undefined at build time; declare every build-time env var explicitly or cache keys will not reflect your configuration.
  24. transpilePackages for Workspace UI: Next.js will not compile raw TSX from node_modules, so when consuming internal monorepo packages like @repo/ui add them to transpilePackages in next.config.ts or imports error at build.
  25. Use workspace: With pnpm:* The workspace:* protocol is only understood by pnpm, yarn, and bun — npm does not parse it; set "packageManager": "pnpm@…" in root package.json and use --frozen-lockfile in CI to avoid drift.