Configuration Basics
8 examples to get you started with Configuration for Node.js backends - 6 basic and 2 intermediate.
Prerequisites
npm install zod
npm install -D typescript@5.6 tsx dotenvNode 24.18.0 reads process.env at runtime. Production values come from the platform (k8s, ECS, Fly.io), not from files in the image.
Basic Examples
1. Twelve-Factor Config in One Module
All settings flow through a single config export.
// src/config.ts
import { z } from "zod";
const envSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
export const config = envSchema.parse(process.env);parsethrows on boot ifDATABASE_URLis missing - fail fast, not on first request- No other file reads
process.envdirectly - Tests set
process.envbefore importingconfigor use dynamic import after setup
Related: Zod & env-schema Validation - typed settings
2. Separate Secrets From Non-Secrets
Secrets rotate more often and need tighter ACLs.
const envSchema = z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(), // secret - connection string
STRIPE_SECRET_KEY: z.string().min(1), // secret - API key
PUBLIC_WEB_URL: z.string().url(), // non-secret - safe in logs
});- Never log
DATABASE_URLor API keys - redact in Pino serializers - Non-secrets can live in ConfigMaps; secrets in Vault/SSM/k8s Secrets
- Same Zod module can tag fields for documentation generators
Related: Secrets Managers - fetch on boot
3. Local .env for Development Only
# .env.example (committed)
PORT=3000
DATABASE_URL=postgres://localhost:5432/app_dev
LOG_LEVEL=debug// src/bootstrap-env.ts - only imported from dev entry
import { config as loadEnv } from "dotenv";
if (process.env.NODE_ENV !== "production") {
loadEnv();
}- Commit
.env.example, never.env - CI and production inject env from the platform
dotenvis a devDependency when possible
Related: dotenv vs Platform Inject - local vs prod
4. Typed Port and Node Environment
Avoid stringly-typed ports in app.listen.
import { config } from "./config";
const app = express();
app.listen(config.PORT, () => {
console.log(`listening on ${config.PORT} env=${config.NODE_ENV}`);
});z.coerce.number()accepts"3000"from k8s string env vars- Branch behavior on
config.NODE_ENV, not scatteredprocess.env.NODE_ENV testenv uses in-memory DB URL from CI secrets
5. Feature Toggles as Config (Static)
Boolean flags from env for simple kill switches.
const envSchema = z.object({
FEATURE_NEW_CHECKOUT: z
.enum(["true", "false"])
.default("false")
.transform((v) => v === "true"),
});- Static env flags require redeploy to flip - fine for rare toggles
- For per-user flags, use a runtime provider (LaunchDarkly, Unleash)
- Document each flag in
.env.examplewith owner and removal date
Related: Feature Flags & Runtime Toggles - dynamic flags
6. Configurable Timeouts and Pool Sizes
Operational tuning belongs in config, not hardcoded magic numbers.
const envSchema = z.object({
HTTP_TIMEOUT_MS: z.coerce.number().default(30_000),
DB_POOL_MAX: z.coerce.number().default(10),
});- Defaults in Zod schema document sane production baselines
- Override in staging to rehearse failure modes
- Alert when prod values drift from documented ranges
Intermediate Examples
7. Lazy Config Singleton With Test Override
let cached: AppConfig | undefined;
export function loadConfig(env = process.env): AppConfig {
if (!cached) cached = envSchema.parse(env);
return cached;
}
export function resetConfigForTests() {
cached = undefined;
}- Tests call
resetConfigForTests()between cases - Production imports
loadConfig()once at boot - Avoids parsing env on every import during hot reload
8. Validate Related Fields With .refine
const envSchema = z
.object({
REDIS_URL: z.string().url().optional(),
CACHE_ENABLED: z.enum(["true", "false"]).transform((v) => v === "true"),
})
.refine((e) => !e.CACHE_ENABLED || e.REDIS_URL, {
message: "REDIS_URL required when CACHE_ENABLED=true",
});- Cross-field rules catch misconfigured staging environments at boot
- Error messages surface in CI deploy logs before traffic routes
- Pair with Configuration Best Practices for team standards
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.