Node.js Environment Configuration: Complete Theory & Practice Guide
Separate configuration from code, validate it, and secure it for real-world deployments.
process.env
dotenv
Security
Table of Contents
1. Theory: Why Environment Configuration Matters
The Twelve-Factor App Principle
Environment configuration is the 3rd factor in the Twelve-Factor App methodology: Store config in the environment.
What is Environment Configuration?
Separating configuration from code by storing settings in environment variables rather than hardcoding them.
Why It's Critical
| Problem | Solution |
|---|---|
| Hardcoded passwords in Git | Environment variables stay out of version control |
| Different settings per environment | .env.dev, .env.prod, .env.test |
| Accidental production database access | Different env vars per environment |
| Configuration changes require redeploy | Change env vars, restart process |
| Team collaboration | Each developer has their own .env.local |
Environment Types
// Common environments in Node.js projects
const environments = {
development: {
purpose: 'Local development',
features: 'Debug logging, hot reload, test APIs',
security: 'Relaxed CORS, mock services'
},
staging: {
purpose: 'Pre-production testing',
features: 'Production-like config, test data',
security: 'Production security but test credentials'
},
production: {
purpose: 'Live user traffic',
features: 'Optimized, minimal logging',
security: 'Strict, real credentials'
},
test: {
purpose: 'Automated testing',
features: 'Isolated database, deterministic',
security: 'Mock credentials'
}
};
2. Basic Process.env Usage
Native Node.js Environment Variables
// basic-env.js - No external packages needed
// Setting environment variables (in terminal before running):
// Mac/Linux: export DB_PASSWORD="secret123" && node basic-env.js
// Windows: set DB_PASSWORD=secret123 && node basic-env.js
// Or inline: DB_PASSWORD=secret123 node basic-env.js
// Reading environment variables
const dbPassword = process.env.DB_PASSWORD;
const nodeEnv = process.env.NODE_ENV || 'development'; // Default value
const port = process.env.PORT || 3000;
console.log(`Environment: ${nodeEnv}`);
console.log(`Port: ${port}`);
console.log(`Database password: ${dbPassword ? '*** Set ***' : 'Not set'}`);
// Complete example with fallbacks
class Config {
constructor() {
this.port = this.getEnv('PORT', 3000);
this.dbHost = this.getEnv('DB_HOST', 'localhost');
this.dbPort = this.getEnv('DB_PORT', 5432);
this.dbName = this.getEnv('DB_NAME', 'app_db');
this.dbUser = this.getEnv('DB_USER', 'admin');
this.dbPassword = this.getEnv('DB_PASSWORD');
this.apiKey = this.getEnv('API_KEY');
this.isProduction = this.getEnv('NODE_ENV') === 'production';
this.isDevelopment = this.getEnv('NODE_ENV') === 'development';
this.isTest = this.getEnv('NODE_ENV') === 'test';
}
getEnv(key, defaultValue = null) {
const value = process.env[key];
if (!value && defaultValue === null) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value || defaultValue;
}
validate() {
const required = ['DB_PASSWORD', 'API_KEY'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}
// Validate types
if (isNaN(parseInt(this.port))) {
throw new Error('PORT must be a number');
}
console.log('ā
Configuration validated successfully');
}
}
// Usage
try {
const config = new Config();
config.validate();
console.log('\nš Current Configuration:');
console.log(` Environment: ${process.env.NODE_ENV || 'development'}`);
console.log(` Port: ${config.port}`);
console.log(` Database: ${config.dbUser}@${config.dbHost}:${config.dbPort}/${config.dbName}`);
console.log(` Production mode: ${config.isProduction}`);
// Simulate app startup
if (config.isDevelopment) {
console.log(' š Debug mode: ENABLED');
}
} catch (error) {
console.error('ā Configuration error:', error.message);
process.exit(1);
}
Running examples
# Missing required variables
$ node basic-env.js
ā Configuration error: Missing required env vars: DB_PASSWORD, API_KEY
# With environment variables
$ DB_PASSWORD=secret123 API_KEY=abc456 NODE_ENV=production node basic-env.js
ā
Configuration validated successfully
š Current Configuration:
Environment: production
Port: 3000
Database: admin@localhost:5432/app_db
Production mode: true
3. dotenv Package Deep Dive
Installation
npm install dotenv
Basic dotenv Usage
// dotenv-basic.js
require('dotenv').config(); // Loads .env file into process.env
console.log('App Name:', process.env.APP_NAME);
console.log('API Key:', process.env.API_KEY);
console.log('Debug Mode:', process.env.DEBUG);
.env file
# .env - Never commit this file!
APP_NAME=MyAwesomeAPI
API_KEY=sk_live_abc123xyz789
DEBUG=true
DB_PASSWORD=supersecret
PORT=5000
Advanced dotenv Configuration
// dotenv-advanced.js
const dotenv = require('dotenv');
const path = require('path');
const fs = require('fs');
class EnvironmentManager {
constructor() {
this.loadEnvFile();
this.validateNodeEnv();
}
loadEnvFile() {
const nodeEnv = process.env.NODE_ENV || 'development';
// Priority order (higher index = higher priority)
const envFiles = [
`.env.${nodeEnv}.local`, // Environment-specific local overrides
`.env.local`, // Local overrides (ignored by git)
`.env.${nodeEnv}`, // Environment-specific
`.env` // Base file
];
console.log(`š Loading environment: ${nodeEnv}`);
for (const envFile of envFiles) {
const envPath = path.resolve(process.cwd(), envFile);
if (fs.existsSync(envPath)) {
console.log(` ā Loading: ${envFile}`);
const result = dotenv.config({ path: envPath, override: true });
if (result.error) {
console.warn(` ā ļø Error loading ${envFile}:`, result.error.message);
}
}
}
// Also load from system environment variables (highest priority)
console.log(' ā System environment variables loaded');
}
validateNodeEnv() {
const allowedEnvs = ['development', 'staging', 'production', 'test'];
const currentEnv = process.env.NODE_ENV || 'development';
if (!allowedEnvs.includes(currentEnv)) {
console.warn(`ā ļø Unknown NODE_ENV: ${currentEnv}, using development`);
process.env.NODE_ENV = 'development';
}
}
printLoadedVars() {
const importantVars = ['NODE_ENV', 'PORT', 'DB_HOST', 'API_URL', 'DEBUG'];
console.log('\nš Loaded Configuration:');
importantVars.forEach(key => {
const value = process.env[key];
if (value) {
const displayValue = key.includes('KEY') || key.includes('SECRET')
? '***HIDDEN***'
: value;
console.log(` ${key}: ${displayValue}`);
}
});
}
}
// Usage
const envManager = new EnvironmentManager();
envManager.printLoadedVars();
// Practical usage in an app
const config = {
port: process.env.PORT || 3000,
database: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
name: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
},
jwtSecret: process.env.JWT_SECRET,
apiKeys: {
stripe: process.env.STRIPE_SECRET_KEY,
sendgrid: process.env.SENDGRID_API_KEY
}
};
// Validate required config
const required = ['DB_PASSWORD', 'JWT_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('\nā Missing required configuration:', missing.join(', '));
process.exit(1);
}
console.log('\nā
Configuration ready for', process.env.NODE_ENV || 'development');
File structure example
project/
āāā .env # Base config (committed with defaults)
āāā .env.example # Template (committed)
āāā .env.local # Local overrides (gitignored)
āāā .env.development # Dev-specific (committed)
āāā .env.production # Prod-specific (gitignored - contains secrets)
āāā .env.test # Test config (committed)
Example file contents
# .env.example (committed to Git)
# Copy this to .env and fill in your values
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_NAME=myapp
JWT_SECRET=change_me
# .env.development (committed)
NODE_ENV=development
DEBUG=true
LOG_LEVEL=debug
API_URL=http://localhost:3000
# .env.local (gitignored)
# Personal overrides
PORT=4000
DB_PASSWORD=my_local_password
# .env.production (gitignored - on server)
NODE_ENV=production
DEBUG=false
LOG_LEVEL=error
DB_HOST=prod-db.example.com
DB_PASSWORD=real_prod_password
JWT_SECRET=very_long_complex_secret
4. Environment-Specific Configs
// env-specific-config.js
const path = require('path');
const fs = require('fs');
class AppConfig {
constructor() {
this.env = process.env.NODE_ENV || 'development';
this.loadConfig();
}
loadConfig() {
// Base config (all environments)
this.base = {
app: {
name: 'MyNodeApp',
version: '1.0.0',
timezone: 'UTC'
},
logging: {
level: 'info',
prettyPrint: this.env === 'development'
}
};
// Environment-specific configs
const envConfigs = {
development: {
server: {
port: 3000,
host: 'localhost',
https: false
},
database: {
host: 'localhost',
port: 5432,
name: 'app_dev',
synchronize: true,
logging: true
},
caching: { enabled: false },
email: { enabled: false, mock: true },
rateLimit: { enabled: false },
debug: {
enabled: true,
showErrors: true,
logQueries: true
}
},
staging: {
server: {
port: 3000,
host: 'staging.myapp.com',
https: true
},
database: {
host: process.env.DB_HOST || 'staging-db.internal',
port: 5432,
name: 'app_staging',
synchronize: false,
logging: true
},
caching: { enabled: true, ttl: 60 },
email: { enabled: true, mock: false },
rateLimit: { enabled: true, maxRequests: 100 },
debug: { enabled: false }
},
production: {
server: {
port: parseInt(process.env.PORT) || 8080,
host: process.env.HOST || '0.0.0.0',
https: true
},
database: {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT) || 5432,
name: process.env.DB_NAME,
synchronize: false,
logging: false,
poolSize: 20
},
caching: { enabled: true, ttl: 300, redis: process.env.REDIS_URL },
email: { enabled: true, mock: false },
rateLimit: { enabled: true, maxRequests: 1000 },
debug: { enabled: false }
},
test: {
server: {
port: 3001,
host: 'localhost',
https: false
},
database: {
host: 'localhost',
port: 5432,
name: 'app_test',
synchronize: true,
logging: false
},
caching: { enabled: false },
email: { enabled: false, mock: true },
rateLimit: { enabled: false },
debug: { enabled: true }
}
};
// Merge base + environment config + environment variables
this.config = this.deepMerge(
this.base,
envConfigs[this.env] || envConfigs.development
);
// Override with environment variables (highest priority)
this.overrideWithEnvVars();
}
deepMerge(target, source) {
const output = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
output[key] = this.deepMerge(target[key] || {}, source[key]);
} else {
output[key] = source[key];
}
}
return output;
}
overrideWithEnvVars() {
// Map environment variable naming to config paths
const mappings = {
PORT: 'server.port',
HOST: 'server.host',
DB_HOST: 'database.host',
DB_PORT: 'database.port',
DB_NAME: 'database.name',
REDIS_URL: 'caching.redis',
LOG_LEVEL: 'logging.level'
};
for (const [envVar, configPath] of Object.entries(mappings)) {
const value = process.env[envVar];
if (value) {
this.setNestedValue(this.config, configPath, value);
}
}
}
setNestedValue(obj, path, value) {
const parts = path.split('.');
const last = parts.pop();
let current = obj;
for (const part of parts) {
if (!current[part]) current[part] = {};
current = current[part];
}
// Type conversion
if (value === 'true') value = true;
if (value === 'false') value = false;
if (!isNaN(value) && value.trim() !== '') value = Number(value);
current[last] = value;
}
get(key) {
return this.getNestedValue(this.config, key);
}
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
isProduction() { return this.env === 'production'; }
isDevelopment() { return this.env === 'development'; }
isTest() { return this.env === 'test'; }
isStaging() { return this.env === 'staging'; }
print() {
console.log(`\nš Configuration (${this.env.toUpperCase()}):`);
console.log('ā'.repeat(50));
const safeConfig = { ...this.config };
// Hide sensitive data when printing
if (safeConfig.database?.password) {
safeConfig.database.password = '***';
}
if (safeConfig.email?.apiKey) {
safeConfig.email.apiKey = '***';
}
console.log(JSON.stringify(safeConfig, null, 2));
}
}
// Usage demonstration
const config = new AppConfig();
config.print();
// Environment-aware logic
if (config.isDevelopment()) {
console.log('\nš Development features enabled:');
console.log(' ⢠Hot reload active');
console.log(' ⢠Detailed error pages');
console.log(' ⢠Mock email service');
}
if (config.isProduction()) {
console.log('\nš Production optimizations:');
console.log(' ⢠Compression enabled');
console.log(' ⢠Cache headers set');
console.log(' ⢠Monitoring active');
}
// Get specific config values
console.log(`\nš” Server will run on: ${config.get('server.host')}:${config.get('server.port')}`);
console.log(`šļø Database: ${config.get('database.name')} at ${config.get('database.host')}`);
console.log(`š Log level: ${config.get('logging.level')}`);
Running with different environments
# Development (default)
$ node env-specific-config.js
# Production
$ NODE_ENV=production DB_HOST=prod-db.example.com DB_NAME=myapp_prod node env-specific-config.js
# Test
$ NODE_ENV=test node env-specific-config.js
5. Validation & Type Safety
Using joi for Schema Validation
npm install joi
// config-validation.js
const Joi = require('joi');
require('dotenv').config();
class ValidatedConfig {
constructor() {
this.schema = this.defineSchema();
this.config = this.validate();
}
defineSchema() {
return Joi.object({
// Server
NODE_ENV: Joi.string()
.valid('development', 'staging', 'production', 'test')
.default('development'),
PORT: Joi.number()
.port()
.default(3000),
HOST: Joi.string()
.hostname()
.default('localhost'),
// Database
DB_HOST: Joi.string()
.required()
.when('NODE_ENV', {
is: 'production',
then: Joi.required(),
otherwise: Joi.default('localhost')
}),
DB_PORT: Joi.number()
.port()
.default(5432),
DB_NAME: Joi.string()
.required(),
DB_USER: Joi.string()
.required(),
DB_PASSWORD: Joi.string()
.min(8)
.required(),
// Security
JWT_SECRET: Joi.string()
.min(32)
.required(),
JWT_EXPIRES_IN: Joi.string()
.default('7d'),
// API Keys
STRIPE_SECRET_KEY: Joi.string()
.when('NODE_ENV', {
is: 'production',
then: Joi.string().pattern(/^sk_live_/).required(),
otherwise: Joi.string().pattern(/^sk_test_/).required()
}),
// Features
DEBUG: Joi.boolean()
.default(false),
LOG_LEVEL: Joi.string()
.valid('error', 'warn', 'info', 'debug')
.default('info'),
CORS_ORIGINS: Joi.string()
.optional(),
REDIS_URL: Joi.string()
.uri({ scheme: ['redis', 'rediss'] })
.optional(),
// Optional with custom validation
RATE_LIMIT_WINDOW_MS: Joi.number()
.integer()
.positive()
.default(900000),
RATE_LIMIT_MAX_REQUESTS: Joi.number()
.integer()
.positive()
.default(100)
});
}
validate() {
// Convert string boolean to actual boolean
const env = { ...process.env };
if (env.DEBUG) env.DEBUG = env.DEBUG === 'true';
const { error, value } = this.schema.validate(env, {
abortEarly: false, // Return all errors, not just first
allowUnknown: true, // Allow extra env vars not in schema
stripUnknown: false // Keep unknown vars
});
if (error) {
console.error('ā Configuration validation failed:');
error.details.forEach(detail => {
console.error(` ⢠${detail.message}`);
});
throw new Error('Invalid configuration');
}
return value;
}
get(key) {
return this.config[key];
}
getDatabaseConfig() {
return {
host: this.config.DB_HOST,
port: this.config.DB_PORT,
database: this.config.DB_NAME,
user: this.config.DB_USER,
password: this.config.DB_PASSWORD,
ssl: this.config.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
};
}
getServerConfig() {
return {
port: this.config.PORT,
host: this.config.HOST,
env: this.config.NODE_ENV,
debug: this.config.DEBUG,
corsOrigins: this.config.CORS_ORIGINS?.split(',') || []
};
}
printSummary() {
console.log('\nā
Configuration Summary:');
console.log('ā'.repeat(40));
console.log(`š Environment: ${this.config.NODE_ENV}`);
console.log(`š Server: ${this.config.HOST}:${this.config.PORT}`);
console.log(`šļø Database: ${this.config.DB_USER}@${this.config.DB_HOST}:${this.config.DB_PORT}/${this.config.DB_NAME}`);
console.log(`š JWT: ${this.config.JWT_SECRET ? 'ā Set' : 'ā Missing'}`);
console.log(`š³ Stripe: ${this.config.STRIPE_SECRET_KEY ? 'ā Configured' : 'ā Not set'}`);
console.log(`š Log Level: ${this.config.LOG_LEVEL}`);
console.log(`š Debug: ${this.config.DEBUG ? 'ON' : 'OFF'}`);
}
}
// Usage with error handling
try {
const config = new ValidatedConfig();
config.printSummary();
// Use in application
const dbConfig = config.getDatabaseConfig();
const serverConfig = config.getServerConfig();
console.log('\nš” Starting server with:');
console.log(` ⢠Environment: ${serverConfig.env}`);
console.log(` ⢠Database SSL: ${dbConfig.ssl ? 'enabled' : 'disabled'}`);
} catch (error) {
console.error('\nā Application startup failed:', error.message);
process.exit(1);
}
Example .env file that passes validation
NODE_ENV=production
PORT=8080
HOST=api.myapp.com
DB_HOST=postgres-prod.internal
DB_NAME=myapp_production
DB_USER=app_user
DB_PASSWORD=strongpassword123!
JWT_SECRET=this_is_a_32_character_secret_key!
STRIPE_SECRET_KEY=sk_live_abc123def456
LOG_LEVEL=error
DEBUG=false
6. Advanced Patterns
Configuration Factory Pattern
// advanced-factory.js
require('dotenv').config();
class ConfigFactory {
static create() {
const env = process.env.NODE_ENV || 'development';
switch(env) {
case 'production':
return new ProductionConfig();
case 'staging':
return new StagingConfig();
case 'test':
return new TestConfig();
default:
return new DevelopmentConfig();
}
}
}
// Base config
class BaseConfig {
constructor() {
this.validate();
}
get(key) {
return process.env[key];
}
validate() {
const required = this.getRequiredKeys();
const missing = required.filter(key => !process.env[key]);
if (missing.length) {
throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}
}
getRequiredKeys() { return []; }
}
class DevelopmentConfig extends BaseConfig {
getRequiredKeys() {
return [];
}
get database() {
return {
host: 'localhost',
port: 5432,
name: 'app_dev',
synchronize: true,
logging: true
};
}
get server() {
return {
port: 3000,
https: false,
cors: { origin: '*' }
};
}
get features() {
return {
caching: false,
emailMock: true,
debugEndpoints: true,
performanceMonitoring: false
};
}
}
class ProductionConfig extends BaseConfig {
getRequiredKeys() {
return ['DB_PASSWORD', 'JWT_SECRET', 'REDIS_URL'];
}
get database() {
return {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT) || 5432,
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
synchronize: false,
logging: false,
pool: { min: 2, max: 10 }
};
}
get server() {
return {
port: parseInt(process.env.PORT) || 8080,
https: true,
cors: { origin: process.env.CORS_ORIGINS?.split(',') || [] }
};
}
get features() {
return {
caching: true,
emailMock: false,
debugEndpoints: false,
performanceMonitoring: true
};
}
get monitoring() {
return {
sentry: process.env.SENTRY_DSN,
datadog: process.env.DATADOG_API_KEY,
newRelic: process.env.NEW_RELIC_KEY
};
}
}
// Similar for StagingConfig and TestConfig...
// Usage
const config = ConfigFactory.create();
console.log('Environment:', process.env.NODE_ENV || 'development');
console.log('Database config:', config.database);
console.log('Features:', config.features);
Secure Configuration with Encryption
npm install crypto-js
// encrypted-config.js
const CryptoJS = require('crypto-js');
const fs = require('fs');
const path = require('path');
class SecureConfig {
constructor() {
this.masterKey = process.env.CONFIG_MASTER_KEY;
if (!this.masterKey) {
throw new Error('CONFIG_MASTER_KEY is required for secure config');
}
this.loadSecrets();
}
loadSecrets() {
const secretsPath = path.join(__dirname, 'secrets.enc');
if (fs.existsSync(secretsPath)) {
const encrypted = fs.readFileSync(secretsPath, 'utf8');
const decrypted = this.decrypt(encrypted);
const secrets = JSON.parse(decrypted);
// Inject into process.env
Object.assign(process.env, secrets);
}
}
encrypt(text) {
return CryptoJS.AES.encrypt(text, this.masterKey).toString();
}
decrypt(ciphertext) {
const bytes = CryptoJS.AES.decrypt(ciphertext, this.masterKey);
return bytes.toString(CryptoJS.enc.Utf8);
}
static createEncryptedFile(secrets) {
const masterKey = crypto.randomBytes(32).toString('hex');
const encrypted = CryptoJS.AES.encrypt(
JSON.stringify(secrets),
masterKey
).toString();
fs.writeFileSync('secrets.enc', encrypted);
console.log('Master key (save this securely):', masterKey);
console.log('Store in environment: export CONFIG_MASTER_KEY=' + masterKey);
}
}
// Usage
try {
const config = new SecureConfig();
console.log('ā
Secure configuration loaded');
} catch (error) {
console.error('Failed to load secure config:', error.message);
}
7. Security Best Practices
Complete Secure Configuration Setup
// secure-best-practices.js
require('dotenv').config();
class SecureConfiguration {
constructor() {
this.validateSecurity();
this.sanitize();
this.freeze();
}
validateSecurity() {
const violations = [];
// Check for hardcoded secrets
const sourceFiles = this.getAllSourceFiles();
sourceFiles.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
if (content.match(/password\s*=\s*['"][^'"]+['"]/i)) {
violations.push(`Hardcoded password in ${file}`);
}
if (content.match(/api[_-]?key\s*=\s*['"][^'"]+['"]/i)) {
violations.push(`Hardcoded API key in ${file}`);
}
});
// Check .gitignore includes .env
if (fs.existsSync('.gitignore')) {
const gitignore = fs.readFileSync('.gitignore', 'utf8');
if (!gitignore.includes('.env')) {
violations.push('.env not in .gitignore - secrets may be committed!');
}
}
// Check production requirements
if (process.env.NODE_ENV === 'production') {
if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) {
violations.push('JWT_SECRET must be at least 32 characters in production');
}
if (process.env.DEBUG === 'true') {
violations.push('DEBUG mode should be disabled in production');
}
if (process.env.DB_PASSWORD && process.env.DB_PASSWORD === 'password') {
violations.push('Weak database password detected');
}
}
if (violations.length) {
console.error('ā Security violations found:');
violations.forEach(v => console.error(` ⢠${v}`));
if (process.env.NODE_ENV === 'production') {
throw new Error('Security violations in production');
}
}
}
sanitize() {
// Remove sensitive data from logs
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
const originalLog = console.log;
console.log = (...args) => {
const sanitized = args.map(arg => {
if (typeof arg === 'string') {
sensitiveKeys.forEach(key => {
const regex = new RegExp(`(${key}\\s*[:=]\\s*)[^\\s,}]+`, 'gi');
arg = arg.replace(regex, `$1[REDACTED]`);
});
}
return arg;
});
originalLog.apply(console, sanitized);
};
}
freeze() {
// Prevent modification of critical config at runtime
const criticalConfig = {
NODE_ENV: process.env.NODE_ENV,
DB_HOST: process.env.DB_HOST,
API_VERSION: process.env.API_VERSION
};
Object.freeze(criticalConfig);
Object.defineProperty(process.env, 'JWT_SECRET', {
configurable: false,
writable: false
});
}
getAllSourceFiles(dir = '.', files = []) {
const items = fs.readdirSync(dir);
items.forEach(item => {
const fullPath = path.join(dir, item);
if (fs.statSync(fullPath).isDirectory()) {
if (!['node_modules', '.git', 'dist', 'build'].includes(item)) {
this.getAllSourceFiles(fullPath, files);
}
} else if (item.endsWith('.js') || item.endsWith('.ts')) {
files.push(fullPath);
}
});
return files;
}
// Helper to generate secure defaults
static generateSecrets() {
const crypto = require('crypto');
return {
JWT_SECRET: crypto.randomBytes(64).toString('hex'),
SESSION_SECRET: crypto.randomBytes(32).toString('hex'),
ENCRYPTION_KEY: crypto.randomBytes(32).toString('base64'),
API_KEY_PREFIX: crypto.randomBytes(8).toString('hex')
};
}
// Docker/K8s friendly config
static getDockerConfig() {
return {
// Read from Docker secrets (Swarm/K8s)
dbPassword: fs.readFileSync('/run/secrets/db_password', 'utf8').trim(),
apiKey: fs.readFileSync('/run/secrets/api_key', 'utf8').trim(),
jwtSecret: fs.readFileSync('/run/secrets/jwt_secret', 'utf8').trim()
};
}
}
// Production-ready config with all best practices
class ProductionReadyApp {
constructor() {
this.config = new SecureConfiguration();
this.setupProcessHandlers();
this.setupConfigWatch();
}
setupProcessHandlers() {
// Re-read config on SIGHUP (config reload)
process.on('SIGHUP', () => {
console.log('Reloading configuration...');
require('dotenv').config({ override: true });
this.config = new SecureConfiguration();
});
// Mask config in crash reports
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error.message);
// Don't log full env in production
if (process.env.NODE_ENV !== 'production') {
console.error(process.env);
}
process.exit(1);
});
}
setupConfigWatch() {
if (process.env.NODE_ENV === 'development') {
fs.watch('.env', (eventType) => {
if (eventType === 'change') {
console.log('š .env changed, reloading...');
require('dotenv').config({ override: true });
}
});
}
}
start() {
console.log('ā
Application configured securely');
console.log(`š¦ Environment: ${process.env.NODE_ENV || 'development'}`);
// Your app logic here
}
}
// Usage
const app = new ProductionReadyApp();
app.start();
// Generate new secrets (run once)
if (require.main === module) {
const secrets = SecureConfiguration.generateSecrets();
console.log('\nš Generated secure secrets:');
Object.entries(secrets).forEach(([key, value]) => {
console.log(`${key}=${value}`);
});
}
Security Checklist
// security-checklist.js
const securityChecklist = {
// ā
Never commit .env files
checkGitignore: () => {
return fs.readFileSync('.gitignore', 'utf8').includes('.env');
},
// ā
Use different secrets per environment
checkEnvironmentSeparation: () => {
return process.env.NODE_ENV !== 'production' ||
(process.env.DB_PASSWORD !== process.env.TEST_DB_PASSWORD);
},
// ā
Rotate secrets regularly
checkSecretAge: (secretFile, maxAgeDays = 90) => {
const stats = fs.statSync(secretFile);
const ageDays = (Date.now() - stats.mtime) / (1000 * 60 * 60 * 24);
return ageDays < maxAgeDays;
},
// ā
Use environment-specific config files
checkEnvFilesExist: () => {
const files = ['.env.development', '.env.production', '.env.test'];
return files.every(file => fs.existsSync(file));
},
// ā
Validate on startup
validateOnStartup: () => {
const required = ['NODE_ENV', 'PORT'];
const missing = required.filter(key => !process.env[key]);
if (missing.length) throw new Error(`Missing: ${missing}`);
},
// ā
Log configuration changes (not secrets)
logConfigChanges: (oldConfig, newConfig) => {
const changes = Object.keys(newConfig).filter(k => oldConfig[k] !== newConfig[k]);
if (changes.length) {
console.log(`Config changed: ${changes.join(', ')}`);
}
}
};
module.exports = { SecureConfiguration, ProductionReadyApp, securityChecklist };
Summary
Key Takeaways
- Never hardcode - Always use environment variables
- Validate everything - Use Joi or similar for type safety
- Environment-specific - Different configs for dev/staging/prod
- Security first - Encrypt secrets, use .gitignore, rotate keys
- Fail fast - Crash if required config is missing
- Document - Provide .env.example with all needed vars
Quick Reference Commands
# Set env var for single command
DB_PASSWORD=secret node app.js
# Export for session (Linux/Mac)
export NODE_ENV=production
# Set for session (Windows)
set NODE_ENV=production
# Use .env file
node -r dotenv/config app.js
# Debug dotenv
node -r dotenv/config app.js --trace-warnings
10 Interview Questions + 10 MCQs
Interview Pattern 10 Q&A1What does Twelve-Factor say about config?easy
Answer: Store configuration in the environment, not in source code.
2Why keep `.env` out of Git?easy
Answer: It can contain secrets like DB passwords and API keys that must not be exposed.
3What is fail-fast config validation?medium
Answer: Validate required vars at startup and terminate immediately if invalid.
4Why is `NODE_ENV` important?medium
Answer: It controls environment-specific behavior like logging, debugging, and optimizations.
5What does Joi add to config management?medium
Answer: Schema-based type validation, defaults, and clear startup error reporting.
6Why separate `.env.development` and `.env.production`?easy
Answer: To prevent accidental cross-environment settings and improve deployment safety.
7When should secrets be rotated?hard
Answer: On a schedule (e.g., 60-90 days) and immediately after exposure or team changes.
8What are dotenv priority layers used for?hard
Answer: To support base defaults plus environment/local overrides with predictable precedence.
9Why mask secrets in logs?hard
Answer: Logs can be widely accessible; masking prevents accidental credential leakage.
10What is config factory pattern?hard
Answer: A design that returns environment-specific config objects from a single creation point.
10 Environment Configuration MCQs
1
Best place for API keys in backend apps?
Explanation: Secrets belong in environment variables or secret managers.
2
Which file should usually be committed?
Explanation: `.env.example` documents required keys safely.
3
Primary purpose of startup validation?
Explanation: Validate config before serving traffic.
4
Which package is used for schema validation here?
Explanation: Joi provides rules, defaults, and type checks.
5
`NODE_ENV=production` should usually imply:
Explanation: Production should be strict and optimized.
6
What is a key risk of hardcoded secrets?
Explanation: Committed secrets are difficult to contain.
7
Why use environment-specific files?
Explanation: Dev/test/prod have different constraints and risk levels.
8
In the examples, encrypted secrets use:
Explanation: The snippet uses `CryptoJS.AES.encrypt/decrypt`.
9
What should happen if required config is missing?
Explanation: Fail-fast prevents unsafe runtime behavior.
10
What does config layering enable?
Explanation: Layering supports portability and controlled overrides.