Configuration Best Practices
One config module; no scattered process.env reads. These rules keep Node.js configuration predictable across dev, CI, and production.
Search across all documentation pages
One config module; no scattered process.env reads. These rules keep Node.js configuration predictable across dev, CI, and production.
process.env outside src/env.ts or src/config.ts.env.example on every PR that adds env varsprocess.env reads in application code. ESLint no-restricted-syntax or custom grep in CI..env.example, never .env. Example lists every key with placeholder values and comments.JWT_SECRET has no default in production schema.z.infer types for factories and tests.redact: ['req.headers.authorization', 'DATABASE_URL']./prod/orders-api/* not /prod/* for every pod..env.example through schema to catch drift..refine.FLAGS.md.Allow in src/env.ts, src/load-env.ts, and test setup files only. Everything else imports config.
Set by platform in prod. Local .env may set development. Schema enum validates allowed values.
Async when fetching SSM/Vault at boot. Top-level await in ESM main.ts on Node 24 is fine.
Per-service env.ts. Shared Zod helpers in internal package - not one global env for all services.
Yes if team standardized. Same rules: one module, boot validation, no scattered reads.
Missing DATABASE_URL in new region deploy - caught by readiness + Zod if schema runs before listen.
No. Set process.env in setup or pass fixture to loadEnv(fixture).
Group in .env.example with comments; link to Notion row for owners. Split schema modules by domain.
Non-secrets like PUBLIC_WEB_URL can live in committed config/default.json if team prefers - still validate with Zod at merge.
Tenant-specific settings yes; process wiring (ports, pool sizes) no - keep in env.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 16, 2026