Difficulty: Beginner
How do you manage environment variables and configuration in a Node.js application?
Environment variables store configuration that changes between environments (development, staging, production) and secrets that should never be committed to version control. In Node.js, they are accessed via process.env.
The dotenv package loads variables from a .env file into process.env during development. In production, variables are set through the hosting platform (Docker, AWS, Heroku). The .env file should be in .gitignore and never committed.
Best practice is to create a config module that reads environment variables, validates them at startup, provides defaults, and exports a typed configuration object. This centralizes all config access and fails fast if required variables are missing.
Sensitive values like API keys, database URLs, and JWT secrets must always be environment variables. Never hardcode them. Use different values per environment and rotate secrets regularly.
// .env file (never commit this!)
// DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
// JWT_SECRET=super-secret-key-change-in-production
// PORT=3000
// NODE_ENV=development
// config.js - centralized configuration
require('dotenv').config();
const config = {
port: parseInt(process.env.PORT, 10) || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
db: {
url: process.env.DATABASE_URL,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '15m',
},
cors: {
origins: (process.env.ALLOWED_ORIGINS || '').split(','),
},
isDev: process.env.NODE_ENV === 'development',
isProd: process.env.NODE_ENV === 'production',
};
module.exports = config;
// Usage
const config = require('./config');
app.listen(config.port);
A config module provides typed access, defaults, and derived values. All env access goes through this single module.
const { z } = require('zod');
require('dotenv').config();
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32, 'JWT secret must be at least 32 chars'),
PORT: z.string().regex(/^\d+$/).transform(Number).default('3000'),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
AWS_BUCKET_NAME: z.string().optional(),
REDIS_URL: z.string().url().optional(),
});
function validateEnv() {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('Environment validation failed:');
console.error(result.error.flatten().fieldErrors);
process.exit(1); // Fail fast!
}
return result.data;
}
const env = validateEnv();
module.exports = env;
// App starts only if all required env vars are valid
// Missing DATABASE_URL? -> crash immediately with clear error
Zod validates env vars at startup. If any required variable is missing or invalid, the app crashes immediately with a clear error instead of failing later.
// .env.development
// DATABASE_URL=postgresql://localhost:5432/myapp_dev
// LOG_LEVEL=debug
// .env.production
// DATABASE_URL=postgresql://prod-host:5432/myapp
// LOG_LEVEL=warn
// .env.example (commit this - documents required vars)
// DATABASE_URL=postgresql://user:pass@host:5432/dbname
// JWT_SECRET=change-me-to-a-long-random-string
// PORT=3000
// Load environment-specific file
const dotenv = require('dotenv');
const path = require('path');
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
dotenv.config({ path: path.resolve(process.cwd(), envFile) });
dotenv.config(); // Fallback to .env
// .gitignore
// .env
// .env.development
// .env.production
// .env.local
// !.env.example <-- DO commit this
console.log(`Loaded config for: ${process.env.NODE_ENV}`);
Use .env.{environment} files for per-environment config. Commit .env.example as documentation. Never commit actual .env files.
dotenv, process.env, Configuration, Secrets Management