Environment Variables (process.env)
Environment variables are key/value settings the operating system hands to your process when it starts. They are the standard way to configure a Node app without hard-coding values — database URLs, API keys, ports, feature flags, and the run mode all come from here. Node copies them onto process.env, where they become the single most common source of configuration in real deployments.
Where they come from
A child process inherits a snapshot of its parent's environment. Your shell sets variables; when it launches node, Node receives a copy. This chain — OS → shell/orchestrator → Node → any child processes you spawn — is why a variable set in one terminal is invisible in another:
The inheritance chain
OS / login session
└─ shell (or Docker / Kubernetes / systemd / CI runner)
└─ node app.js ← gets a COPY of the parent's env
└─ child_process ← gets a COPY of Node's env (tweak via {env})Reading variables
console.log(process.env.NODE_ENV) // 'development' | 'production' | undefined const port = process.env.PORT || 3000 const dbUrl = process.env.DATABASE_URL
Boolean('false') // true ← the trap
process.env.FEATURE_X === 'true' // correct boolean check
Number(process.env.MAX || '100') // correct numeric parse with defaultReading vs writing
Setting variables when you run
Shell | Command |
|---|---|
macOS / Linux (bash/zsh) |
|
Windows PowerShell |
|
Windows cmd.exe |
|
Cross-platform (npm script) |
|
The .env file pattern
Listing variables on the command line gets unwieldy fast. The convention is a .env file in the project root. Modern Node loads it natively with --env-file; older versions use the dotenv package.
.env
PORT=8080 NODE_ENV=development DATABASE_URL=postgres://localhost:5432/app JWT_SECRET=super-secret-value
Native (Node 20.6+) — no dependency needed
node --env-file=.env app.js
Or with dotenv (older Node)
import 'dotenv/config' // loads .env into process.env at startup
NODE_ENV and run modes
By convention NODE_ENV is 'production' in prod and 'development' locally. Many libraries (Express, React) optimize or add safety checks based on it — Express caches view templates and skips verbose errors when it is 'production':
const isProd = process.env.NODE_ENV === 'production' app.use(isProd ? compression() : devLogger())
Security: never commit secrets
Validate required vars at startup and fail fast if any are missing.
Never log full secret values — mask them (
sk_live_****) if you must log presence.Use a secrets manager (Vault, AWS/GCP Secrets Manager) for sensitive production data.
Scope secrets to the smallest surface — a leaked
.envin a build log is a real incident. See Dependency & Secret Security.
Validating at startup
Fail loudly the moment the process boots, not deep inside a request handler at 3am. A tiny guard turns a confusing runtime crash into a clear startup error:
const required = ['DATABASE_URL', 'JWT_SECRET']
const missing = required.filter((key) => !process.env[key])
if (missing.length > 0) {
console.error(`Missing required env vars: ${missing.join(', ')}`)
process.exit(1)
}Missing required env vars: JWT_SECRET