Skip to content

Environment Variables in Express

Environment variables keep configuration and secrets, database credentials, API keys, ports, out of your source code, letting the same codebase run correctly across development, staging, and production. This lesson covers managing them with dotenv.

Why Environment Variables Matter

Hardcoding a database password or API key directly in source code means it ends up in version control, visible to anyone with repository access, forever, even after you "remove" it in a later commit. Environment variables keep this information outside the codebase entirely.

In development, the dotenv package loads variables from a local .env file into process.env; in production, most hosting platforms let you set environment variables directly through their dashboard or CLI, with no .env file needed at all.

// .env (never committed to version control)
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=super-secret-value

// server.js
require('dotenv').config();

const PORT = process.env.PORT || 3000;
app.listen(PORT);

dotenv.config() reads .env and merges its keys into process.env, this call should happen as early as possible in your app's startup.

Reading Environment Variables Safely

const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET;

if (!JWT_SECRET) {
  throw new Error('JWT_SECRET environment variable is required');
}
  • process.env values are always strings, cast numbers/booleans explicitly when needed.
  • Provide sensible defaults for non-sensitive config (like PORT), but never for secrets.
  • Fail fast at startup if a required secret is missing, rather than failing confusingly later.
  • Add .env to .gitignore immediately, before the first commit that touches it.

Environment Variables Cheatsheet

Common configuration values kept in environment variables.

Variable Purpose
PORT Port the server listens on
NODE_ENV development, production, or test
DATABASE_URL Full database connection string
JWT_SECRET Secret key for signing/verifying JWTs
SESSION_SECRET Secret key for signing session cookies
ALLOWED_ORIGIN CORS-allowed frontend origin

Validating Required Environment Variables at Startup

Rather than discovering a missing secret when a route unexpectedly fails, validate required environment variables once, at startup, and crash immediately with a clear message if anything is missing.

const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET', 'SESSION_SECRET'];

for (const key of requiredEnvVars) {
  if (!process.env[key]) {
    console.error(`Missing required environment variable: ${key}`);
    process.exit(1);
  }
}

Per-Environment Config Files

Some teams keep separate .env.development, .env.test, and .env.production files (with only the non-production ones ever committed as .env.example templates), loaded conditionally based on NODE_ENV.

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV || 'development'}`,
});

Common Mistakes

  • Committing a real .env file (with actual secrets) to version control.
  • Hardcoding a fallback value for a genuine secret, silently running with an insecure default.
  • Forgetting to call dotenv.config() before code that reads process.env, especially in files loaded early.
  • Not validating required environment variables, letting the app start in a broken, half-configured state.

Key Takeaways

  • Environment variables separate configuration and secrets from source code.
  • dotenv loads a local .env file into process.env during development.
  • Never commit a real .env file, commit a .env.example template instead.
  • Validate required environment variables at startup, failing fast with a clear error.

Pro Tip

Commit a .env.example file listing every variable name your app expects (with placeholder values, no real secrets), it doubles as living documentation for anyone setting up the project for the first time.