Environment variables configure applications across environments. Here's how to use them effectively.
Basic Usage#
1// Access environment variables
2const port = process.env.PORT;
3const nodeEnv = process.env.NODE_ENV;
4const apiKey = process.env.API_KEY;
5
6console.log(`Server running on port ${port}`);
7
8// Set environment variables (command line)
9// PORT=3000 node app.js
10// NODE_ENV=production node app.js
11
12// Multiple variables
13// PORT=3000 NODE_ENV=production API_KEY=abc123 node app.js
14
15// Windows (PowerShell)
16// $env:PORT=3000; node app.js
17
18// Windows (Command Prompt)
19// set PORT=3000 && node app.js
20
21// Cross-platform (use cross-env package)
22// npx cross-env PORT=3000 node app.jsUsing dotenv#
1// Install: npm install dotenv
2
3// Load at app start
4require('dotenv').config();
5
6// Or with ES modules
7import 'dotenv/config';
8
9// Or manually
10import dotenv from 'dotenv';
11dotenv.config();
12
13// .env file
14/*
15PORT=3000
16NODE_ENV=development
17DATABASE_URL=postgres://localhost:5432/mydb
18API_KEY=your-secret-key
19DEBUG=true
20*/
21
22// Access variables
23const config = {
24 port: process.env.PORT || 3000,
25 env: process.env.NODE_ENV || 'development',
26 dbUrl: process.env.DATABASE_URL,
27 apiKey: process.env.API_KEY,
28 debug: process.env.DEBUG === 'true',
29};
30
31// Multiple .env files
32dotenv.config({ path: '.env.local' });
33dotenv.config({ path: `.env.${process.env.NODE_ENV}` });Configuration Module#
1// config.js
2import dotenv from 'dotenv';
3
4dotenv.config();
5
6function required(key) {
7 const value = process.env[key];
8 if (!value) {
9 throw new Error(`Missing required environment variable: ${key}`);
10 }
11 return value;
12}
13
14function optional(key, defaultValue) {
15 return process.env[key] ?? defaultValue;
16}
17
18function boolean(key, defaultValue = false) {
19 const value = process.env[key];
20 if (value === undefined) return defaultValue;
21 return value === 'true' || value === '1';
22}
23
24function number(key, defaultValue) {
25 const value = process.env[key];
26 if (value === undefined) return defaultValue;
27 const parsed = parseInt(value, 10);
28 if (isNaN(parsed)) {
29 throw new Error(`Environment variable ${key} must be a number`);
30 }
31 return parsed;
32}
33
34export const config = {
35 env: optional('NODE_ENV', 'development'),
36 port: number('PORT', 3000),
37
38 database: {
39 url: required('DATABASE_URL'),
40 poolSize: number('DB_POOL_SIZE', 10),
41 },
42
43 auth: {
44 jwtSecret: required('JWT_SECRET'),
45 jwtExpiry: optional('JWT_EXPIRY', '24h'),
46 },
47
48 api: {
49 key: required('API_KEY'),
50 rateLimit: number('RATE_LIMIT', 100),
51 },
52
53 features: {
54 debug: boolean('DEBUG'),
55 analytics: boolean('ENABLE_ANALYTICS', true),
56 },
57
58 isDev: process.env.NODE_ENV === 'development',
59 isProd: process.env.NODE_ENV === 'production',
60 isTest: process.env.NODE_ENV === 'test',
61};
62
63// Usage
64import { config } from './config.js';
65
66console.log(config.port);
67console.log(config.database.url);TypeScript Configuration#
1// config.ts
2import dotenv from 'dotenv';
3import { z } from 'zod';
4
5dotenv.config();
6
7// Schema definition
8const envSchema = z.object({
9 NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
10 PORT: z.string().transform(Number).default('3000'),
11 DATABASE_URL: z.string().url(),
12 JWT_SECRET: z.string().min(32),
13 API_KEY: z.string(),
14 DEBUG: z.string().transform(v => v === 'true').default('false'),
15 RATE_LIMIT: z.string().transform(Number).default('100'),
16});
17
18// Parse and validate
19const envResult = envSchema.safeParse(process.env);
20
21if (!envResult.success) {
22 console.error('Invalid environment variables:');
23 console.error(envResult.error.format());
24 process.exit(1);
25}
26
27export const env = envResult.data;
28
29// Type-safe access
30export const config = {
31 env: env.NODE_ENV,
32 port: env.PORT,
33 database: {
34 url: env.DATABASE_URL,
35 },
36 auth: {
37 jwtSecret: env.JWT_SECRET,
38 },
39 api: {
40 key: env.API_KEY,
41 rateLimit: env.RATE_LIMIT,
42 },
43 debug: env.DEBUG,
44 isDev: env.NODE_ENV === 'development',
45 isProd: env.NODE_ENV === 'production',
46} as const;
47
48export type Config = typeof config;Environment-Specific Files#
# File structure
project/
├── .env # Default/development
├── .env.local # Local overrides (gitignored)
├── .env.development # Development specific
├── .env.production # Production specific
├── .env.test # Test specific
└── .env.example # Template (committed)
1// Load in order
2import dotenv from 'dotenv';
3import path from 'path';
4
5const env = process.env.NODE_ENV || 'development';
6
7// Load files in order (later files override earlier)
8dotenv.config({ path: path.resolve(process.cwd(), '.env') });
9dotenv.config({ path: path.resolve(process.cwd(), `.env.${env}`) });
10dotenv.config({ path: path.resolve(process.cwd(), '.env.local') });
11dotenv.config({ path: path.resolve(process.cwd(), `.env.${env}.local`) });
12
13// Or use dotenv-flow
14import dotenvFlow from 'dotenv-flow';
15dotenvFlow.config();Secrets Management#
1// Never commit secrets to git
2// .gitignore
3/*
4.env
5.env.local
6.env.*.local
7*/
8
9// Use .env.example for documentation
10/*
11# .env.example
12DATABASE_URL=postgres://user:pass@localhost:5432/db
13JWT_SECRET=your-secret-key-here-minimum-32-chars
14API_KEY=your-api-key
15*/
16
17// Production secrets via environment
18// Heroku: heroku config:set API_KEY=xxx
19// AWS: Parameter Store or Secrets Manager
20// Docker: --env-file or secrets
21
22// Example: AWS Secrets Manager
23import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
24
25async function getSecrets() {
26 const client = new SecretsManagerClient({ region: 'us-east-1' });
27
28 const response = await client.send(
29 new GetSecretValueCommand({ SecretId: 'my-app/production' })
30 );
31
32 return JSON.parse(response.SecretString);
33}
34
35// Load secrets in production
36if (process.env.NODE_ENV === 'production') {
37 const secrets = await getSecrets();
38 Object.assign(process.env, secrets);
39}Docker Configuration#
1# Dockerfile
2FROM node:18-alpine
3
4WORKDIR /app
5
6COPY package*.json ./
7RUN npm ci --only=production
8
9COPY . .
10
11# Don't include .env in image
12# Pass at runtime
13
14EXPOSE 3000
15CMD ["node", "server.js"]1# docker-compose.yml
2version: '3.8'
3
4services:
5 app:
6 build: .
7 ports:
8 - '3000:3000'
9 environment:
10 - NODE_ENV=production
11 - PORT=3000
12 env_file:
13 - .env.production
14 secrets:
15 - db_password
16 - api_key
17
18secrets:
19 db_password:
20 file: ./secrets/db_password.txt
21 api_key:
22 file: ./secrets/api_key.txtTesting with Environment Variables#
1// test/setup.js
2process.env.NODE_ENV = 'test';
3process.env.DATABASE_URL = 'postgres://localhost:5432/test_db';
4process.env.JWT_SECRET = 'test-secret-minimum-32-characters';
5
6// Jest configuration
7// jest.config.js
8module.exports = {
9 setupFiles: ['<rootDir>/test/setup.js'],
10};
11
12// Or use dotenv in test setup
13import dotenv from 'dotenv';
14dotenv.config({ path: '.env.test' });
15
16// Mock environment variables in tests
17describe('config', () => {
18 const originalEnv = process.env;
19
20 beforeEach(() => {
21 process.env = { ...originalEnv };
22 });
23
24 afterAll(() => {
25 process.env = originalEnv;
26 });
27
28 it('should use default port', () => {
29 delete process.env.PORT;
30 const config = loadConfig();
31 expect(config.port).toBe(3000);
32 });
33
34 it('should use custom port', () => {
35 process.env.PORT = '8080';
36 const config = loadConfig();
37 expect(config.port).toBe(8080);
38 });
39});Validation and Defaults#
1// Comprehensive validation
2class ConfigError extends Error {
3 constructor(message, key) {
4 super(message);
5 this.name = 'ConfigError';
6 this.key = key;
7 }
8}
9
10function validateConfig() {
11 const errors = [];
12
13 // Required variables
14 const required = ['DATABASE_URL', 'JWT_SECRET', 'API_KEY'];
15
16 for (const key of required) {
17 if (!process.env[key]) {
18 errors.push(`Missing required: ${key}`);
19 }
20 }
21
22 // Format validation
23 if (process.env.PORT && isNaN(parseInt(process.env.PORT))) {
24 errors.push('PORT must be a number');
25 }
26
27 if (process.env.JWT_SECRET && process.env.JWT_SECRET.length < 32) {
28 errors.push('JWT_SECRET must be at least 32 characters');
29 }
30
31 if (process.env.DATABASE_URL && !process.env.DATABASE_URL.startsWith('postgres://')) {
32 errors.push('DATABASE_URL must be a valid PostgreSQL connection string');
33 }
34
35 if (errors.length > 0) {
36 console.error('Configuration errors:');
37 errors.forEach(err => console.error(` - ${err}`));
38 process.exit(1);
39 }
40}
41
42// Call on startup
43validateConfig();Best Practices#
Security:
✓ Never commit .env files
✓ Use .env.example as template
✓ Rotate secrets regularly
✓ Use secret managers in production
Organization:
✓ Group related variables
✓ Use consistent naming (UPPER_SNAKE_CASE)
✓ Document all variables
✓ Provide sensible defaults
Validation:
✓ Validate on startup
✓ Fail fast on missing required vars
✓ Type-check values
✓ Use schema validation (zod)
Development:
✓ Use dotenv for local dev
✓ Keep .env.example updated
✓ Test configuration loading
✓ Mock env vars in tests
Conclusion#
Environment variables are essential for configuration management. Use dotenv for local development, validate configuration on startup, and never commit secrets. Implement a robust configuration module with type safety and validation for production applications.