Environment variables and .env files

Node.js · Essentials · 9 min

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.js

For 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-exists doesn'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"), or util.parseEnv(text) to parse without touching process.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.env values are strings or undefined — convert explicitly
  • node --env-file=.env replaces the dotenv package
  • Validate all config once at startup and fail fast

Watch out for

  • Committing .env with real secrets to git.
  • Writing if (process.env.FEATURE) and enabling the feature when it is set to "false".

Check yourself

  1. process.env.ENABLE_CACHE is set to the string "false". What does `if (process.env.ENABLE_CACHE)` do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nodejs.org/docs/latest-v22.x/api/environment_variables.html