Next.js Cheatsheet

Environment and Config

Use this Next.js reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Environment Variable Files

Next.js loads .env files automatically. Files are loaded in this priority order (higher overrides lower):

FileWhen loadedCommitted?
.env.localAlwaysNo (gitignore)
.env.development.localnpm run dev onlyNo
.env.test.localNODE_ENV=test onlyNo
.env.production.localnpm run build/startNo
.env.developmentnpm run dev onlyYes
.env.testNODE_ENV=test onlyYes
.env.productionnpm run build/startYes
.envAll environmentsYes

.env.local is always the highest priority and is never committed. Use it for secrets.

Server vs. Browser Variables

# .env.local

# Server-only (NOT sent to browser)
DATABASE_URL=postgresql://...
JWT_SECRET=supersecretkey
STRIPE_SECRET_KEY=sk_live_...

# Browser-visible (must prefix with NEXT_PUBLIC_)
NEXT_PUBLIC_APP_URL=https://myapp.com
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_live_...
// Server Component / Route Handler / Middleware
const secret = process.env.JWT_SECRET        // works

// Client Component
const appUrl = process.env.NEXT_PUBLIC_APP_URL   // works
const secret = process.env.JWT_SECRET            // undefined (never sent to client)

NEXT_PUBLIC_ variables are inlined at build time — they are hardcoded into the JS bundle. Do not put secrets here.

Accessing Variables

// Server Component
export default async function Page() {
  const db = process.env.DATABASE_URL!    // string | undefined
  return <div>DB configured: {!!db}</div>
}
// Client Component
'use client'
export function Footer() {
  return <p>{process.env.NEXT_PUBLIC_APP_URL}</p>
}

Type Safety for Env Vars

Use a validation file loaded at startup:

// lib/env.ts  (import this in layout.tsx or server entry)
import { z } from 'zod'

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  NEXT_PUBLIC_APP_URL: z.string().url(),
  NODE_ENV: z.enum(['development', 'test', 'production']),
})

export const env = envSchema.parse(process.env)

Or use the @t3-oss/env-nextjs package for a typed, validated env with NEXT_PUBLIC_ support.

next.config.ts — Full Reference

import type { NextConfig } from 'next'

const config: NextConfig = {
  // ─── Output / Build ───────────────────────────────────────────
  output: 'standalone',      // self-contained output (Docker)
  // output: 'export',       // static HTML export (no server needed)
  distDir: '.next',          // custom build output directory

  // ─── Images ───────────────────────────────────────────────────
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'example.com' },
    ],
    formats: ['image/avif', 'image/webp'],   // default: webp only
    minimumCacheTTL: 60,                     // seconds
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    unoptimized: false,
  },

  // ─── Redirects / Rewrites / Headers ──────────────────────────
  async redirects() {
    return [
      { source: '/old/:slug', destination: '/new/:slug', permanent: true },
    ]
  },

  async rewrites() {
    return {
      beforeFiles: [],   // checked before filesystem/pages
      afterFiles: [],    // checked after filesystem, before fallbacks
      fallback: [],      // last resort
    }
  },

  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
        ],
      },
    ]
  },

  // ─── TypeScript / ESLint ──────────────────────────────────────
  typescript: {
    ignoreBuildErrors: false,    // true to skip type errors at build (dangerous)
  },
  eslint: {
    ignoreDuringBuilds: false,
    dirs: ['app', 'components', 'lib'],
  },

  // ─── Env ──────────────────────────────────────────────────────
  env: {
    // Expose build-time constants (accessible as process.env.VAR anywhere)
    BUILD_TIME: new Date().toISOString(),
  },

  // ─── Webpack customization ────────────────────────────────────
  webpack(config, { isServer }) {
    if (!isServer) {
      // Don't bundle server-only packages client-side
      config.resolve.fallback = { fs: false, path: false }
    }
    return config
  },

  // ─── React ───────────────────────────────────────────────────
  reactStrictMode: true,
  reactCompiler: true,           // React Compiler (auto-memo) — stable top-level in Next 16

  // ─── Compiler ────────────────────────────────────────────────
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
    // styledComponents: true,
    // emotion: true,
  },

  // ─── Server packages ─────────────────────────────────────────
  // Opt packages out of Server Component bundling (use require at runtime).
  // Top-level since Next 15 (was experimental.serverComponentsExternalPackages).
  serverExternalPackages: ['@prisma/client'],

  // ─── Cache Components (Next 16, beta) ────────────────────────
  // Enables the 'use cache' directive + cacheLife/cacheTag.
  // Replaces the Next 15 experimental ppr / dynamicIO flags.
  // cacheComponents: true,

  // ─── Logging ─────────────────────────────────────────────────
  logging: {
    fetches: {
      fullUrl: true,    // log full URLs in dev
    },
  },

  // ─── Page Extensions ─────────────────────────────────────────
  pageExtensions: ['tsx', 'ts', 'jsx', 'js'],   // default

  // ─── Trailing Slash ──────────────────────────────────────────
  trailingSlash: false,    // true → /about/ instead of /about

  // ─── Base Path ───────────────────────────────────────────────
  // basePath: '/docs',   // serve the app under a sub-path
}

export default config

Static Export (output: 'export')

Generates a fully static site — no Node.js server needed.

// next.config.ts
export default { output: 'export' }

Limitations: - No Server Actions (no server) - No Route Handlers at runtime - No ISR (static only) - No next/image optimization (use unoptimized: true) - No middleware

npm run build   # generates out/ directory
npx serve out   # serve locally to test

Standalone Output (Docker)

export default { output: 'standalone' }
FROM node:20-alpine
COPY .next/standalone ./
COPY .next/static ./.next/static
COPY public ./public
CMD node server.js

instrumentation.ts — Observability Hook

Stable since Next 15 — just create the file, no config flag needed (the old experimental.instrumentationHook is gone).

// instrumentation.ts (root level)
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    // Node.js-only setup (OpenTelemetry, Sentry, etc.)
    const { NodeSDK } = await import('@opentelemetry/sdk-node')
    new NodeSDK().start()
  }
}

export async function onRequestError(
  err: Error,
  request: { path: string; method: string },
  context: { routerKind: string; routePath: string }
) {
  // Report errors to your monitoring service
  await reportError(err, { path: request.path })
}

tsconfig.json Key Settings for Next.js

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./*"] }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Environment Variable Gotchas

GotchaDetail
NEXT_PUBLIC_ vars are baked in at buildChanging them requires a rebuild
Dynamic runtime env varsUse process.env.VAR in Route Handlers/Server Actions — not in layout/page module scope
.env.local not in DockerMount it as a volume or use Docker secrets / CI env
undefined in browserAny var without NEXT_PUBLIC_ is undefined in client code
next.config.ts env keyExposes vars to both server and client — use only for non-secret build constants