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 Config Matters
  2. Basic Process.env Usage
  3. dotenv Package Deep Dive
  4. Environment-Specific Configs
  5. Validation & Type Safety
  6. Advanced Patterns
  7. Security Best Practices
  8. Summary
  9. Interview Q&A + MCQ
  10. Contextual Learning Links

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

ProblemSolution
Hardcoded passwords in GitEnvironment variables stay out of version control
Different settings per environment.env.dev, .env.prod, .env.test
Accidental production database accessDifferent env vars per environment
Configuration changes require redeployChange env vars, restart process
Team collaborationEach 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&A
1What 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?

AHardcoded source files
BEnvironment variables
CPublic README
DClient-side JavaScript
Explanation: Secrets belong in environment variables or secret managers.
2

Which file should usually be committed?

A.env
B.env.production with secrets
C.env.example
D.env.local with personal keys
Explanation: `.env.example` documents required keys safely.
3

Primary purpose of startup validation?

AImprove CSS rendering
BFail fast on invalid config
CReduce bundle size
DIncrease DB speed
Explanation: Validate config before serving traffic.
4

Which package is used for schema validation here?

Awinston
Bjoi
Cexpress
Daxios
Explanation: Joi provides rules, defaults, and type checks.
5

`NODE_ENV=production` should usually imply:

AVerbose debug logging
BRelaxed security rules
CStrict security + optimized behavior
DMock credentials
Explanation: Production should be strict and optimized.
6

What is a key risk of hardcoded secrets?

ABetter readability
BAccidental exposure in version control
CFaster startup
DSmaller memory use
Explanation: Committed secrets are difficult to contain.
7

Why use environment-specific files?

ATo share one password everywhere
BTo keep behavior appropriate per stage
CTo disable validation
DTo remove NODE_ENV
Explanation: Dev/test/prod have different constraints and risk levels.
8

In the examples, encrypted secrets use:

Acrypto-js AES
BBase64 only
CURL encoding
Dgzip
Explanation: The snippet uses `CryptoJS.AES.encrypt/decrypt`.
9

What should happen if required config is missing?

AContinue silently
BUse random defaults for secrets
CCrash startup with clear error
DIgnore in production only
Explanation: Fail-fast prevents unsafe runtime behavior.
10

What does config layering enable?

AOne universal static file
BBase defaults with local/env overrides
CNo need for NODE_ENV
DClient-side secret storage
Explanation: Layering supports portability and controlled overrides.