Environment variables configure applications across environments. Here's how to manage them securely and effectively.
Basic Usage#
1// Access environment variables
2const port = process.env.PORT || 3000;
3const dbUrl = process.env.DATABASE_URL;
4const nodeEnv = process.env.NODE_ENV;
5
6// Required variables should fail fast
7const apiKey = process.env.API_KEY;
8if (!apiKey) {
9 throw new Error('API_KEY environment variable is required');
10}
11
12// .env file (development only)
13// DATABASE_URL=postgresql://localhost:5432/myapp
14// API_KEY=dev_key_123
15// NODE_ENV=developmentType-Safe Configuration#
1import { z } from 'zod';
2
3// Define schema
4const envSchema = z.object({
5 NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
6 PORT: z.string().transform(Number).default('3000'),
7 DATABASE_URL: z.string().url(),
8 REDIS_URL: z.string().url().optional(),
9 API_KEY: z.string().min(1),
10 JWT_SECRET: z.string().min(32),
11 CORS_ORIGINS: z.string().transform((s) => s.split(',')).default(''),
12 LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
13 ENABLE_METRICS: z.string().transform((s) => s === 'true').default('false'),
14});
15
16// Parse and validate
17function loadEnv() {
18 const result = envSchema.safeParse(process.env);
19
20 if (!result.success) {
21 console.error('Invalid environment variables:');
22 console.error(result.error.format());
23 process.exit(1);
24 }
25
26 return result.data;
27}
28
29export const env = loadEnv();
30
31// Usage
32console.log(env.PORT); // number
33console.log(env.NODE_ENV); // 'development' | 'production' | 'test'
34console.log(env.CORS_ORIGINS); // string[]Environment Files#
1# .env.example (committed to repo)
2NODE_ENV=development
3PORT=3000
4DATABASE_URL=postgresql://localhost:5432/myapp
5API_KEY=your_api_key_here
6JWT_SECRET=generate_a_secure_secret
7
8# .env (local development - NOT committed)
9NODE_ENV=development
10PORT=3000
11DATABASE_URL=postgresql://localhost:5432/myapp_dev
12API_KEY=dev_key_123
13JWT_SECRET=dev_secret_at_least_32_characters
14
15# .env.test (test environment)
16NODE_ENV=test
17DATABASE_URL=postgresql://localhost:5432/myapp_test
18
19# .env.production (production template - secrets from secret manager)
20NODE_ENV=production
21PORT=8080
22# DATABASE_URL and secrets come from cloud provider1# .gitignore
2.env
3.env.local
4.env.*.local
5.env.production
6!.env.exampleLoading Environment Variables#
1// dotenv (Node.js)
2import 'dotenv/config';
3
4// Or with path
5import { config } from 'dotenv';
6config({ path: '.env.local' });
7
8// Next.js (automatic)
9// .env.local is loaded automatically
10// Access via process.env.NEXT_PUBLIC_* for client-side
11
12// Vite
13// .env, .env.local, .env.[mode]
14// Access via import.meta.env.VITE_*Secrets Management#
1// Never commit secrets
2// ❌ Bad
3const apiKey = 'sk_live_abc123'; // Hardcoded secret
4
5// ✓ Good
6const apiKey = process.env.API_KEY;
7
8// Use secret managers in production
9import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
10
11const client = new SecretsManagerClient({});
12
13async function getSecret(secretId: string): Promise<Record<string, string>> {
14 const command = new GetSecretValueCommand({ SecretId: secretId });
15 const response = await client.send(command);
16 return JSON.parse(response.SecretString!);
17}
18
19// Load secrets at startup
20async function loadSecrets() {
21 const secrets = await getSecret('production/app');
22 process.env.DATABASE_URL = secrets.DATABASE_URL;
23 process.env.API_KEY = secrets.API_KEY;
24}
25
26// Or use environment variables set by infrastructure
27// AWS ECS, Kubernetes, etc. can inject secrets as env varsValidation Patterns#
1// Validate on startup
2function validateEnv(): void {
3 const required = [
4 'DATABASE_URL',
5 'JWT_SECRET',
6 'API_KEY',
7 ];
8
9 const missing = required.filter((key) => !process.env[key]);
10
11 if (missing.length > 0) {
12 throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
13 }
14
15 // Validate formats
16 if (process.env.JWT_SECRET!.length < 32) {
17 throw new Error('JWT_SECRET must be at least 32 characters');
18 }
19
20 if (!process.env.DATABASE_URL!.startsWith('postgresql://')) {
21 throw new Error('DATABASE_URL must be a PostgreSQL connection string');
22 }
23}
24
25// Call early in application startup
26validateEnv();Configuration Object#
1// config.ts - Centralized configuration
2interface Config {
3 env: 'development' | 'production' | 'test';
4 port: number;
5 database: {
6 url: string;
7 poolSize: number;
8 };
9 redis: {
10 url: string;
11 } | null;
12 auth: {
13 jwtSecret: string;
14 jwtExpiresIn: string;
15 };
16 cors: {
17 origins: string[];
18 };
19}
20
21function loadConfig(): Config {
22 return {
23 env: (process.env.NODE_ENV as Config['env']) || 'development',
24 port: parseInt(process.env.PORT || '3000', 10),
25 database: {
26 url: requireEnv('DATABASE_URL'),
27 poolSize: parseInt(process.env.DB_POOL_SIZE || '10', 10),
28 },
29 redis: process.env.REDIS_URL
30 ? { url: process.env.REDIS_URL }
31 : null,
32 auth: {
33 jwtSecret: requireEnv('JWT_SECRET'),
34 jwtExpiresIn: process.env.JWT_EXPIRES_IN || '1h',
35 },
36 cors: {
37 origins: (process.env.CORS_ORIGINS || '').split(',').filter(Boolean),
38 },
39 };
40}
41
42function requireEnv(key: string): string {
43 const value = process.env[key];
44 if (!value) {
45 throw new Error(`Missing required environment variable: ${key}`);
46 }
47 return value;
48}
49
50export const config = loadConfig();
51
52// Usage
53import { config } from './config';
54console.log(config.database.url);
55console.log(config.auth.jwtSecret);Docker and Kubernetes#
1# Dockerfile
2FROM node:20-alpine
3
4WORKDIR /app
5COPY . .
6RUN npm ci --only=production
7
8# Don't include .env in image
9# Pass env vars at runtime
10CMD ["node", "dist/index.js"]1# docker-compose.yml
2services:
3 app:
4 build: .
5 environment:
6 - NODE_ENV=production
7 - PORT=3000
8 env_file:
9 - .env.production
10 # Or from shell
11 # environment:
12 # - DATABASE_URL=${DATABASE_URL}1# kubernetes secret
2apiVersion: v1
3kind: Secret
4metadata:
5 name: app-secrets
6type: Opaque
7stringData:
8 DATABASE_URL: postgresql://user:pass@host:5432/db
9 JWT_SECRET: your-secret-here
10
11---
12# kubernetes deployment
13apiVersion: apps/v1
14kind: Deployment
15spec:
16 template:
17 spec:
18 containers:
19 - name: app
20 envFrom:
21 - secretRef:
22 name: app-secrets
23 env:
24 - name: NODE_ENV
25 value: productionBest Practices#
Security:
✓ Never commit secrets
✓ Use .env.example for documentation
✓ Use secret managers in production
✓ Rotate secrets regularly
Validation:
✓ Validate on startup
✓ Fail fast on missing required vars
✓ Use type-safe configuration
✓ Provide sensible defaults
Organization:
✓ Use consistent naming (SCREAMING_SNAKE_CASE)
✓ Group related variables
✓ Document all variables
✓ Use environment-specific files
Conclusion#
Environment variables separate configuration from code. Use .env files for local development, secret managers for production, and always validate configuration at startup. Type-safe configuration objects prevent runtime errors and improve developer experience.