Configuration Best Practices
One config module; no scattered process.env reads. These rules keep Node.js configuration predictable across dev, CI, and production.
How to Use This List
- Add CI grep for
process.envoutsidesrc/env.tsorsrc/config.ts - Review
.env.exampleon every PR that adds env vars - Pair with Zod & env-schema Validation
- Assign flag and secret owners in team runbook
A - Single Source of Truth
- One module exports validated config. All settings flow through Zod (or equivalent) parse at boot.
- Ban direct
process.envreads in application code. ESLintno-restricted-syntaxor custom grep in CI. - Commit
.env.example, never.env. Example lists every key with placeholder values and comments. - Defaults document dev ergonomics, not prod secrets.
JWT_SECREThas no default in production schema. - Export
z.infertypes for factories and tests.
B - Secrets Hygiene
- Secrets load from vault/SSM/k8s in production. Not from files in the container image.
- Redact secrets in log serializers. Pino
redact: ['req.headers.authorization', 'DATABASE_URL']. - Least-privilege IAM per service path.
/prod/orders-api/*not/prod/*for every pod. - Rotate without code change. New secret version + rolling restart; no hardcoded key IDs in repo.
- Never print env on startup for debugging in staging - use key names only.
C - Environment Parity
- Same schema validates dev, test, staging, prod. Different values, same keys (minus optional integrations).
- CI parses
.env.examplethrough schema to catch drift. - Block localhost DB URLs in production schema via
.refine. - Staging uses production-like secret mechanics (real SSM path in staging account).
- Preview envs document ephemeral URLs in platform dashboard, not Slack pins.
D - Feature Flags and Toggles
- Risky behavior gated server-side. Client flags are hints; API enforces authorization paths.
- Default off when flag provider unreachable for payment, auth, and data deletion flows.
- Every flag has owner and removal date in provider or
FLAGS.md. - Prefer runtime flags over env for product experiments that change more than weekly.
- Audit flags quarterly - delete shipped flags and dead branches.
E - Operations
- Fail readiness until config and secrets load. Do not serve traffic with partial env.
- Document required env in deployment manifest next to k8s/ECS task definition.
- Version config schema changes in changelog when renames break deploy templates.
- Graceful shutdown before secret rotation drains in-flight DB connections using old URL.
- 12-factor: config in env, not argv or JSON files in image for portable containers.
FAQs
How strict should the process.env ban be?
Allow in src/env.ts, src/load-env.ts, and test setup files only. Everything else imports config.
What about NODE_ENV?
Set by platform in prod. Local .env may set development. Schema enum validates allowed values.
Should config be sync or async?
Async when fetching SSM/Vault at boot. Top-level await in ESM main.ts on Node 24 is fine.
How do monorepos share config?
Per-service env.ts. Shared Zod helpers in internal package - not one global env for all services.
Can I use convict instead of Zod?
Yes if team standardized. Same rules: one module, boot validation, no scattered reads.
What is the top config incident cause?
Missing DATABASE_URL in new region deploy - caught by readiness + Zod if schema runs before listen.
Should tests use .env file?
No. Set process.env in setup or pass fixture to loadEnv(fixture).
How do I document 40 env vars?
Group in .env.example with comments; link to Notion row for owners. Split schema modules by domain.
Are public config values OK in git?
Non-secrets like PUBLIC_WEB_URL can live in committed config/default.json if team prefers - still validate with Zod at merge.
When does config belong in database?
Tenant-specific settings yes; process wiring (ports, pool sizes) no - keep in env.
Related
- Configuration Basics - intro examples
- Zod & env-schema Validation - schema patterns
- dotenv vs Platform Inject - source by environment
- Secrets Managers - production secrets
- Feature Flags & Runtime Toggles - dynamic config
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.