Next.js Cheatsheet

Environment and Config

Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

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