Configuration Basics
8 examples to get you started with Configuration for Node.js backends - 6 basic and 2 intermediate.
Search across all documentation pages
8 examples to get you started with Configuration for Node.js backends - 6 basic and 2 intermediate.
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.
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);parse throws on boot if DATABASE_URL is missing - fail fast, not on first requestprocess.env directlyprocess.env before importing config or use dynamic import after setupRelated: Zod & env-schema Validation - typed settings
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
});DATABASE_URL or API keys - redact in Pino serializersRelated: Secrets Managers - fetch on boot
.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();
}.env.example, never .envdotenv is a devDependency when possibleRelated: dotenv vs Platform Inject - local vs prod
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 varsconfig.NODE_ENV, not scattered process.env.NODE_ENVtest env uses in-memory DB URL from CI secretsBoolean flags from env for simple kill switches.
const envSchema = z.object({
FEATURE_NEW_CHECKOUT: z
.enum(["true", "false"])
.default("false")
.transform((v) => v === "true"),
});.env.example with owner and removal dateRelated: Feature Flags & Runtime Toggles - dynamic flags
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),
});let cached: AppConfig | undefined;
export function loadConfig(env = process.env): AppConfig {
if (!cached) cached = envSchema.parse(env);
return cached;
}
export function resetConfigForTests() {
cached = undefined;
}resetConfigForTests() between casesloadConfig() once at boot.refineconst 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",
});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