Environment variables store configuration outside your code — database URLs, API keys, port numbers — keeping secrets out of source control.

Reading Environment Variables

  const port = process.env.PORT || 3000;
const nodeEnv = process.env.NODE_ENV || 'development';

console.log(`Running on port ${port} in ${nodeEnv} mode`);
  

Setting Variables

  # Linux/macOS
PORT=8080 NODE_ENV=production node app.js

# Windows (cmd)
set PORT=8080 && node app.js

# Windows (PowerShell)
$env:PORT=8080; node app.js
  

Using dotenv

Install:

  npm install dotenv
  

Create .env (never commit to git):

  PORT=3000
NODE_ENV=development
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key-here
API_KEY=abc123
  

Load in your app:

  import 'dotenv/config'; // ES modules — loads at import

// Or
import dotenv from 'dotenv';
dotenv.config();

const port = process.env.PORT;
const dbUrl = process.env.DATABASE_URL;
  

.env.example

Commit a template without secrets:

  PORT=3000
NODE_ENV=development
DATABASE_URL=
JWT_SECRET=
  

Developers copy to .env and fill in values.

.gitignore

  .env
.env.local
.env.*.local
  

Validating Environment Variables

Fail fast if required variables are missing:

  const required = ['PORT', 'DATABASE_URL', 'JWT_SECRET'];

for (const key of required) {
    if (!process.env[key]) {
        throw new Error(`Missing required environment variable: ${key}`);
    }
}
  

Or use zod / envalid for schema validation:

  import { cleanEnv, str, num } from 'envalid';

const env = cleanEnv(process.env, {
    PORT: num({ default: 3000 }),
    DATABASE_URL: str(),
    JWT_SECRET: str()
});
  

NODE_ENV Values

Value Purpose
development Local dev, verbose logging
production Optimized, minimal logging
test Running tests

Many libraries change behavior based on NODE_ENV.

Best Practices

  • Never hardcode secrets in source code
  • Use different .env files per environment
  • Rotate secrets regularly
  • Use secret managers (AWS Secrets Manager, Vault) in production