The configuration problem
The same server runs in several environments: your laptop (development),
a test server (staging), and production (production). Each one needs
different values: the port, the database URL, the keys for external APIs...
If you hardcode those values in the code, you mix what changes
(the config) with what doesn't (the program), and you end up with an if (environment === "prod")
scattered everywhere.
The 12-factor methodology
The Twelve-Factor App is a set of best practices for building services. Its factor III (Config) says:
Store configuration in the environment, not in the code.
Specifically:
- Config lives in environment variables (
process.env), not in the repository. - The same build (the same code and artifact) is deployed across all environments; the only thing that changes is the environment around it.
- A good test: could you open source the repo right now without leaking any credential? If the answer is no, you have secrets in the code.
// In Node, environment variables arrive in process.env (always strings).
const PORT = process.env.PORT; // "3000"
const NODE_ENV = process.env.NODE_ENV; // "production"
Centralize and set default values
Instead of reading process.env all over the code, centralize the reading in a
single config module that validates and applies default values. That way the rest
of the application receives a clean, typed object:
function loadConfig(env) {
return {
port: Number(env.PORT) || 3000,
environment: env.NODE_ENV || "development",
};
}
const config = loadConfig(process.env);
In these exercises we inject the environment as an
envobject (instead of readingprocess.envdirectly). This makes the config testable: you can test the same function with{}, with{ PORT: "8080" }, etc.
Separate config per environment
The value of NODE_ENV decides the behavior: in development you want detailed
logs and full error messages; in production, compact logs and generic errors
(so as not to leak internal details). The code is the same; only
the values it receives change.
Secret management
Passwords, tokens, and keys are especially sensitive config:
- They are never committed. Locally a
.envfile is used (ignored by git, loaded with a library likedotenv); in production they are injected by the platform (Docker, Kubernetes Secrets, AWS Secrets Manager, Vault...). - The repo includes a
.env.examplewith the keys without values, as documentation. - The application must fail on startup (fail fast) if a required secret is missing, instead of breaking in the middle of a request.
Examples
Centralized config with defaults and secret validation
function loadConfig(env) {
if (env.NODE_ENV === "production" && !env.DATABASE_URL) {
throw new Error("DATABASE_URL is missing in production");
}
return {
port: Number(env.PORT) || 3000,
environment: env.NODE_ENV || "development",
dbUrl: env.DATABASE_URL || "memory://local",
};
}
console.log(loadConfig({ PORT: "8080" }));
console.log(loadConfig({}));