Configuration Management
⚙️ Configuration Management
The Problem With process.env
Node's process.env is typed as { [key: string]: string | undefined } — every single value might be missing, and there's no way to know until runtime:
Silent Typos
process.env.DATBASE_URL (typo) just returns undefined — no error, no warning, until something fails downstream.
Everything Is a String
PORT=3000 arrives as the string "3000", not the number 3000 — every numeric or boolean value needs explicit parsing.
One Schema, One Type — Again
The same pattern from Chapter 3's request validation applies directly to configuration: define a schema once, derive the type from it, and parse process.env through it:
z.coerce.number() and z.coerce.boolean() handle the "everything is a string" problem directly in the schema — "3000" becomes the number 3000, not just a validated string.
🚦 Validate at Startup, Fail Fast
The whole point is to move a missing/invalid config value from "crashes mid-request in production" to "crashes immediately on boot, with a clear message":
Lazy Access vs Startup Validation
Lazy Access
Config is read wherever it's needed, whenever it's needed.
Startup Validation
Config is parsed once, before the server even starts listening.
A Typed Config Module
🔐 Secrets Management
Secrets are configuration too — but they need extra care that ordinary config doesn't:
Never Commit Secrets
.env holds local development secrets and is .gitignore'd; .env.example documents the required keys with placeholder values.
Production Secret Stores
AWS Secrets Manager, HashiCorp Vault, or your platform's built-in secrets — injected as environment variables at deploy time, never checked into the repo.
Separate Schema for Secrets
Splitting PublicConfig from Secrets at the type level makes it obvious which values are safe to log and which aren't.
Rotation
Config validated at startup also means a rotated secret is caught immediately on the next deploy, not silently ignored.
💻 Coding Challenges
Challenge 1: Type process.env
Write a ConfigSchema for NODE_ENV, PORT, and LOG_LEVEL (an enum of "debug" | "info" | "warn" | "error"), then derive a Config type from it.
Goal: Practice coercing string env vars into numbers/enums through a schema.
Challenge 2: Fail Fast on Boot
Write a loadConfig() function that parses process.env against your schema and calls process.exit(1) with a readable error message if validation fails.
Goal: Practice turning a runtime config problem into an immediate, loud startup failure instead of a silent one.
Challenge 3: Separate Public Config From Secrets
Split your schema into PublicConfigSchema and SecretsSchema, and write a function that logs only the public config, never the secrets.
Goal: Practice using separate types to make "safe to log" a compile-time guarantee.
console.log(config) feels harmless during debugging — until config includes a database password or API key, and that log line ends up in a log aggregator (see the upcoming Logging & Observability chapter) that dozens of people can search. Keep secrets in a separate typed object from public config, and never pass the combined object to a logger.
🎯 What's Next
With configuration validated and secrets kept separate, the next chapter looks at what happens once the app is actually running: Logging & Observability — structured logging, log aggregation, and the basics of metrics and tracing for debugging production issues.