Twelve-factor apps take secrets and settings from the environment, so the same code runs in dev, CI and production.
Configuration that differs between environments — database URLs, API keys, ports, feature flags — belongs in environment variables, not in code. Node exposes them as process.env, a plain object whose values are always strings (or undefined if unset).
PORT=8080 LOG_LEVEL=debug node server.jsFor local development you usually keep them in a .env file. Node 22 reads it natively, no dotenv package required:
node --env-file=.env server.js— fails if the file is missing;--env-file-if-existsdoesn't.- Pass the flag several times; later files override earlier ones.
- Variables already set in the real environment win over the file, so production settings can't be overridden by a stray
.env. - From code:
process.loadEnvFile(".env"), orutil.parseEnv(text)to parse without touchingprocess.env.
Commit a .env.example listing every variable with dummy values, and add .env to .gitignore.
Read and validate configuration once, at startup, in one module. Convert types explicitly — Number(process.env.PORT), process.env.DEBUG === "true" — because "false" is a truthy string. Fail fast with a clear message if something required is missing; a crash at boot is much better than a undefined connection string discovered at 3 a.m.
NODE_ENV=production is a convention many libraries (Express included) check to disable debug features and enable caching. Set it in production, but don't use it as your only switch for app behaviour — explicit variables are clearer.
Example
// config.js — run: node --env-file=.env main.js
function required(name) {
const value = process.env[name];
if (value === undefined || value === "") {
throw new Error(`Missing required environment variable ${name}`);
}
return value;
}
export const config = Object.freeze({
port: Number(process.env.PORT ?? 3000),
databaseUrl: required("DATABASE_URL"),
debug: process.env.DEBUG === "true", // "false" would be truthy as a string!
env: process.env.NODE_ENV ?? "development",
});
/* .env
PORT=8080
DATABASE_URL=postgres://localhost/app
DEBUG=true
*/Remember
process.envvalues are strings or undefined — convert explicitlynode --env-file=.envreplaces the dotenv package- Validate all config once at startup and fail fast
Watch out for
- Committing
.envwith real secrets to git. - Writing
if (process.env.FEATURE)and enabling the feature when it is set to "false".