Environment Variables in Node.js
Environment Variables in Node.js
Section titled “Environment Variables in Node.js”Environment variables externalise configuration that changes between environments (development, staging, production) — database URLs, API keys, feature flags, port numbers.
Basic Usage
Section titled “Basic Usage”// Access any environment variableconst port = process.env.PORT;const dbUrl = process.env.DATABASE_URL;const nodeEnv = process.env.NODE_ENV; // 'development', 'production', 'test'All values are strings — parse numbers and booleans explicitly:
const port = parseInt(process.env.PORT ?? '3000', 10);const debugMode = process.env.DEBUG === 'true';const maxRetries = Number(process.env.MAX_RETRIES ?? '3');.env Files with dotenv
Section titled “.env Files with dotenv”npm install dotenv# .env (never commit this file)PORT=3000NODE_ENV=developmentDATABASE_URL=postgresql://localhost:5432/mydbJWT_SECRET=my-secret-keyAPI_KEY=sk-abc123// Load as early as possible — before any other imports that need env varsimport 'dotenv/config'; // ESM
// or:require('dotenv').config(); // CommonJS.env File Conventions
Section titled “.env File Conventions”# .env — local secrets, gitignored# .env.example — committed, shows what variables are needed (no real values)# .env.test — values for test environment# .env.production — never committed; set via CI/CD or hosting platform.env.example (commit this):
PORT=3000NODE_ENV=developmentDATABASE_URL=postgresql://localhost:5432/mydb_devJWT_SECRET=change-meTyped Config Module
Section titled “Typed Config Module”Centralise and validate all env vars in one place:
function requireEnv(name: string): string { const value = process.env[name]; if (!value) throw new Error(`Missing required environment variable: ${name}`); return value;}
export const config = { port: parseInt(process.env.PORT ?? '3000', 10), nodeEnv: process.env.NODE_ENV ?? 'development', database: { url: requireEnv('DATABASE_URL'), poolSize: parseInt(process.env.DB_POOL_SIZE ?? '10', 10), }, jwt: { secret: requireEnv('JWT_SECRET'), expiresIn: process.env.JWT_EXPIRES_IN ?? '7d', }, api: { key: requireEnv('API_KEY'), },} as const;This approach:
- Fails fast at startup if required variables are missing
- Gives you autocomplete throughout the codebase
- Keeps all config logic in one place
Validation with Zod
Section titled “Validation with Zod”npm install zodimport { z } from 'zod';
const envSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), PORT: z.string().transform(Number).default('3000'), DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'), LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),});
const parsed = envSchema.safeParse(process.env);if (!parsed.success) { console.error('Invalid environment variables:', parsed.error.flatten()); process.exit(1);}
export const env = parsed.data;Setting Variables Per Environment
Section titled “Setting Variables Per Environment”Locally:
# .env file (dotenv handles this)PORT=3000Shell (one-off):
PORT=4000 NODE_ENV=production node dist/server.jsGitHub Actions:
- name: Run tests env: DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} JWT_SECRET: ${{ secrets.JWT_SECRET }} run: npm testDocker:
docker run -e NODE_ENV=production -e PORT=3000 my-app
# Or from a filedocker run --env-file .env.production my-appNever Commit Secrets
Section titled “Never Commit Secrets”Add to .gitignore:
.env.env.local.env.production*.pemFor production, use:
- Azure: App Service configuration / Key Vault
- AWS: Parameter Store / Secrets Manager
- GCP: Secret Manager
- Kubernetes: Secrets + Workload Identity
Multiple Environments
Section titled “Multiple Environments”// Different .env files per environment// dotenv-flow handles this automaticallynpm install dotenv-flow
// Loads .env, then .env.{NODE_ENV}, then .env.{NODE_ENV}.local// More specific files override earlier ones