Skip to content

Configuration

Overview

Configuration management is crucial for Node.js applications that need to run in different environments (development, testing, staging, production). This chapter covers best practices for managing configuration, environment variables, and application settings.

Environment Variables

Basic Environment Variables

Environment variables provide a way to configure your application without hardcoding values:

javascript
// Basic usage
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL || 'mongodb://localhost:27017/myapp';
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret';

console.log('Server will run on port:', port);
console.log('Database URL:', dbUrl);

Using dotenv

The dotenv package loads environment variables from a .env file:

bash
npm install dotenv

Create environment files:

bash
# .env (development)
NODE_ENV=development
PORT=3000
HOST=localhost

# Database
DATABASE_URL=mongodb://localhost:27017/myapp_dev
DB_HOST=localhost
DB_PORT=27017
DB_NAME=myapp_dev
DB_USER=
DB_PASSWORD=

# Redis
REDIS_URL=redis://localhost:6379
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# JWT
JWT_SECRET=your-development-secret-key
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=7d

# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password

# File Storage
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=5242880
ALLOWED_FILE_TYPES=jpg,jpeg,png,gif,pdf,doc,docx

# External APIs
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
SENDGRID_API_KEY=SG...

# Logging
LOG_LEVEL=debug
LOG_FILE=./logs/app.log

# Security
BCRYPT_ROUNDS=12
RATE_LIMIT_WINDOW=900000
RATE_LIMIT_MAX=100
bash
# .env.production
NODE_ENV=production
PORT=8080
HOST=0.0.0.0

# Database (use connection string in production)
DATABASE_URL=mongodb+srv://user:password@cluster.mongodb.net/myapp_prod

# Redis
REDIS_URL=redis://redis-server:6379

# JWT (use strong secrets in production)
JWT_SECRET=super-secure-production-secret-key-change-this
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d

# Email
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=your-sendgrid-api-key

# File Storage (use cloud storage in production)
UPLOAD_DIR=/app/uploads
MAX_FILE_SIZE=10485760
ALLOWED_FILE_TYPES=jpg,jpeg,png,pdf

# External APIs
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PUBLISHABLE_KEY=pk_live_...

# Logging
LOG_LEVEL=info
LOG_FILE=/var/log/app.log

# Security
BCRYPT_ROUNDS=14
RATE_LIMIT_WINDOW=900000
RATE_LIMIT_MAX=50

Loading Environment Variables

javascript
// config/env.js
const dotenv = require('dotenv');
const path = require('path');

// Determine which .env file to load
const envFile = process.env.NODE_ENV === 'production' 
  ? '.env.production' 
  : process.env.NODE_ENV === 'test' 
    ? '.env.test' 
    : '.env';

// Load environment variables
const result = dotenv.config({ path: path.resolve(process.cwd(), envFile) });

if (result.error && process.env.NODE_ENV !== 'production') {
  console.warn(`Warning: Could not load ${envFile} file`);
}

// Validate required environment variables
const requiredEnvVars = [
  'JWT_SECRET',
  'DATABASE_URL'
];

const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]);

if (missingEnvVars.length > 0) {
  console.error('Missing required environment variables:', missingEnvVars);
  process.exit(1);
}

module.exports = {
  loaded: !result.error,
  envFile,
  requiredEnvVars
};

Configuration Object

Centralized Configuration

Create a centralized configuration object:

javascript
// config/index.js
require('./env'); // Load environment variables first

const config = {
  // Application
  app: {
    name: process.env.APP_NAME || 'Node.js App',
    version: process.env.APP_VERSION || '1.0.0',
    env: process.env.NODE_ENV || 'development',
    port: parseInt(process.env.PORT, 10) || 3000,
    host: process.env.HOST || 'localhost',
    url: process.env.APP_URL || `http://localhost:${process.env.PORT || 3000}`
  },

  // Database
  database: {
    url: process.env.DATABASE_URL,
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT, 10) || 27017,
    name: process.env.DB_NAME || 'myapp',
    user: process.env.DB_USER || '',
    password: process.env.DB_PASSWORD || '',
    ssl: process.env.DB_SSL === 'true',
    poolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 10,
    options: {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      maxPoolSize: parseInt(process.env.DB_POOL_SIZE, 10) || 10,
      serverSelectionTimeoutMS: 5000,
      socketTimeoutMS: 45000,
    }
  },

  // Redis
  redis: {
    url: process.env.REDIS_URL,
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT, 10) || 6379,
    password: process.env.REDIS_PASSWORD || '',
    db: parseInt(process.env.REDIS_DB, 10) || 0,
    keyPrefix: process.env.REDIS_KEY_PREFIX || 'myapp:',
    ttl: parseInt(process.env.REDIS_TTL, 10) || 3600
  },

  // JWT
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '24h',
    refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
    issuer: process.env.JWT_ISSUER || 'myapp',
    audience: process.env.JWT_AUDIENCE || 'myapp-users'
  },

  // Email
  email: {
    smtp: {
      host: process.env.SMTP_HOST,
      port: parseInt(process.env.SMTP_PORT, 10) || 587,
      secure: process.env.SMTP_SECURE === 'true',
      auth: {
        user: process.env.SMTP_USER,
        pass: process.env.SMTP_PASSWORD
      }
    },
    from: {
      name: process.env.EMAIL_FROM_NAME || 'MyApp',
      address: process.env.EMAIL_FROM_ADDRESS || 'noreply@myapp.com'
    },
    templates: {
      welcome: 'welcome',
      resetPassword: 'reset-password',
      emailVerification: 'email-verification'
    }
  },

  // File Upload
  upload: {
    dir: process.env.UPLOAD_DIR || './uploads',
    maxSize: parseInt(process.env.MAX_FILE_SIZE, 10) || 5 * 1024 * 1024, // 5MB
    allowedTypes: (process.env.ALLOWED_FILE_TYPES || 'jpg,jpeg,png,gif,pdf').split(','),
    storage: process.env.STORAGE_TYPE || 'local', // local, s3, cloudinary
    s3: {
      bucket: process.env.S3_BUCKET,
      region: process.env.S3_REGION || 'us-east-1',
      accessKeyId: process.env.S3_ACCESS_KEY_ID,
      secretAccessKey: process.env.S3_SECRET_ACCESS_KEY
    }
  },

  // Security
  security: {
    bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS, 10) || 12,
    rateLimit: {
      windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10) || 15 * 60 * 1000, // 15 minutes
      max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100,
      message: 'Too many requests from this IP'
    },
    cors: {
      origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : true,
      credentials: process.env.CORS_CREDENTIALS === 'true'
    },
    helmet: {
      contentSecurityPolicy: process.env.NODE_ENV === 'production',
      crossOriginEmbedderPolicy: false
    }
  },

  // Logging
  logging: {
    level: process.env.LOG_LEVEL || 'info',
    file: process.env.LOG_FILE || './logs/app.log',
    maxSize: process.env.LOG_MAX_SIZE || '20m',
    maxFiles: parseInt(process.env.LOG_MAX_FILES, 10) || 5,
    format: process.env.LOG_FORMAT || 'json'
  },

  // External APIs
  apis: {
    stripe: {
      secretKey: process.env.STRIPE_SECRET_KEY,
      publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
      webhookSecret: process.env.STRIPE_WEBHOOK_SECRET
    },
    sendgrid: {
      apiKey: process.env.SENDGRID_API_KEY
    },
    twilio: {
      accountSid: process.env.TWILIO_ACCOUNT_SID,
      authToken: process.env.TWILIO_AUTH_TOKEN,
      phoneNumber: process.env.TWILIO_PHONE_NUMBER
    }
  }
};

// Environment-specific overrides
if (config.app.env === 'test') {
  config.database.name = config.database.name + '_test';
  config.logging.level = 'error';
}

if (config.app.env === 'production') {
  config.logging.level = 'info';
  config.security.rateLimit.max = 50; // Stricter rate limiting in production
}

module.exports = config;

Configuration Validation

Schema Validation

Use a schema validation library like Joi:

bash
npm install joi
javascript
// config/validation.js
const Joi = require('joi');

const configSchema = Joi.object({
  app: Joi.object({
    name: Joi.string().required(),
    version: Joi.string().required(),
    env: Joi.string().valid('development', 'test', 'staging', 'production').required(),
    port: Joi.number().port().required(),
    host: Joi.string().required(),
    url: Joi.string().uri().required()
  }).required(),

  database: Joi.object({
    url: Joi.string().required(),
    host: Joi.string().required(),
    port: Joi.number().port().required(),
    name: Joi.string().required(),
    user: Joi.string().allow(''),
    password: Joi.string().allow(''),
    ssl: Joi.boolean(),
    poolSize: Joi.number().min(1).max(100)
  }).required(),

  jwt: Joi.object({
    secret: Joi.string().min(32).required(),
    expiresIn: Joi.string().required(),
    refreshExpiresIn: Joi.string().required(),
    issuer: Joi.string().required(),
    audience: Joi.string().required()
  }).required(),

  email: Joi.object({
    smtp: Joi.object({
      host: Joi.string().required(),
      port: Joi.number().port().required(),
      secure: Joi.boolean(),
      auth: Joi.object({
        user: Joi.string().required(),
        pass: Joi.string().required()
      }).required()
    }).required(),
    from: Joi.object({
      name: Joi.string().required(),
      address: Joi.string().email().required()
    }).required()
  }).required(),

  security: Joi.object({
    bcryptRounds: Joi.number().min(10).max(15).required(),
    rateLimit: Joi.object({
      windowMs: Joi.number().min(1000).required(),
      max: Joi.number().min(1).required(),
      message: Joi.string().required()
    }).required()
  }).required(),

  logging: Joi.object({
    level: Joi.string().valid('error', 'warn', 'info', 'debug').required(),
    file: Joi.string().required(),
    maxSize: Joi.string().required(),
    maxFiles: Joi.number().min(1).required(),
    format: Joi.string().valid('json', 'simple').required()
  }).required()
});

function validateConfig(config) {
  const { error, value } = configSchema.validate(config, {
    allowUnknown: true,
    abortEarly: false
  });

  if (error) {
    const errorMessages = error.details.map(detail => detail.message);
    throw new Error(`Configuration validation failed:\n${errorMessages.join('\n')}`);
  }

  return value;
}

module.exports = {
  configSchema,
  validateConfig
};

Using Validation

javascript
// config/index.js (updated)
const { validateConfig } = require('./validation');

// ... config object definition ...

// Validate configuration
try {
  const validatedConfig = validateConfig(config);
  module.exports = validatedConfig;
} catch (error) {
  console.error('Configuration Error:', error.message);
  process.exit(1);
}

Dynamic Configuration

Configuration Loader

javascript
// config/loader.js
const fs = require('fs');
const path = require('path');

class ConfigLoader {
  constructor() {
    this.config = {};
    this.watchers = new Map();
  }

  load(configPath) {
    try {
      const fullPath = path.resolve(configPath);
      
      if (!fs.existsSync(fullPath)) {
        throw new Error(`Configuration file not found: ${fullPath}`);
      }

      const configData = fs.readFileSync(fullPath, 'utf8');
      const parsedConfig = JSON.parse(configData);
      
      this.config = { ...this.config, ...parsedConfig };
      return this.config;
    } catch (error) {
      throw new Error(`Failed to load configuration: ${error.message}`);
    }
  }

  watch(configPath, callback) {
    const fullPath = path.resolve(configPath);
    
    if (this.watchers.has(fullPath)) {
      return;
    }

    const watcher = fs.watchFile(fullPath, (curr, prev) => {
      if (curr.mtime !== prev.mtime) {
        try {
          this.load(configPath);
          callback(this.config);
        } catch (error) {
          console.error('Error reloading configuration:', error.message);
        }
      }
    });

    this.watchers.set(fullPath, watcher);
  }

  get(key, defaultValue = null) {
    const keys = key.split('.');
    let value = this.config;

    for (const k of keys) {
      if (value && typeof value === 'object' && k in value) {
        value = value[k];
      } else {
        return defaultValue;
      }
    }

    return value;
  }

  set(key, value) {
    const keys = key.split('.');
    let current = this.config;

    for (let i = 0; i < keys.length - 1; i++) {
      const k = keys[i];
      if (!(k in current) || typeof current[k] !== 'object') {
        current[k] = {};
      }
      current = current[k];
    }

    current[keys[keys.length - 1]] = value;
  }

  stopWatching() {
    for (const [path, watcher] of this.watchers) {
      fs.unwatchFile(path);
    }
    this.watchers.clear();
  }
}

module.exports = ConfigLoader;

Feature Flags

Feature Flag System

javascript
// config/features.js
class FeatureFlags {
  constructor(config = {}) {
    this.flags = new Map();
    this.loadFlags(config);
  }

  loadFlags(config) {
    // Load from environment variables
    Object.keys(process.env).forEach(key => {
      if (key.startsWith('FEATURE_')) {
        const flagName = key.replace('FEATURE_', '').toLowerCase();
        const flagValue = process.env[key] === 'true';
        this.flags.set(flagName, flagValue);
      }
    });

    // Load from config object
    if (config.features) {
      Object.entries(config.features).forEach(([key, value]) => {
        this.flags.set(key.toLowerCase(), value);
      });
    }
  }

  isEnabled(flagName) {
    return this.flags.get(flagName.toLowerCase()) || false;
  }

  enable(flagName) {
    this.flags.set(flagName.toLowerCase(), true);
  }

  disable(flagName) {
    this.flags.set(flagName.toLowerCase(), false);
  }

  toggle(flagName) {
    const current = this.isEnabled(flagName);
    this.flags.set(flagName.toLowerCase(), !current);
  }

  getAllFlags() {
    return Object.fromEntries(this.flags);
  }

  // Middleware for Express
  middleware() {
    return (req, res, next) => {
      req.features = this;
      next();
    };
  }
}

// Usage in environment variables
// FEATURE_NEW_UI=true
// FEATURE_BETA_API=false
// FEATURE_ANALYTICS=true

module.exports = FeatureFlags;

Using Feature Flags

javascript
// app.js
const FeatureFlags = require('./config/features');
const config = require('./config');

const features = new FeatureFlags(config);

// Use feature flags in middleware
app.use(features.middleware());

// Use in routes
app.get('/api/users', (req, res) => {
  if (req.features.isEnabled('beta_api')) {
    // Use new API version
    return res.json({ version: 'v2', users: [] });
  }
  
  // Use old API version
  res.json({ version: 'v1', users: [] });
});

// Use in services
class UserService {
  async getUsers() {
    if (features.isEnabled('new_user_query')) {
      return this.getUsersOptimized();
    }
    return this.getUsersLegacy();
  }
}

Configuration Best Practices

Secrets Management

javascript
// config/secrets.js
const fs = require('fs');
const path = require('path');

class SecretsManager {
  constructor() {
    this.secrets = new Map();
    this.loadSecrets();
  }

  loadSecrets() {
    // Load from environment variables
    this.loadFromEnv();
    
    // Load from files (Docker secrets, Kubernetes secrets)
    this.loadFromFiles();
    
    // Load from external services (AWS Secrets Manager, HashiCorp Vault)
    // this.loadFromExternalService();
  }

  loadFromEnv() {
    const secretEnvVars = [
      'JWT_SECRET',
      'DATABASE_PASSWORD',
      'STRIPE_SECRET_KEY',
      'SENDGRID_API_KEY'
    ];

    secretEnvVars.forEach(envVar => {
      if (process.env[envVar]) {
        this.secrets.set(envVar.toLowerCase(), process.env[envVar]);
      }
    });
  }

  loadFromFiles() {
    const secretsDir = process.env.SECRETS_DIR || '/run/secrets';
    
    if (!fs.existsSync(secretsDir)) {
      return;
    }

    const secretFiles = fs.readdirSync(secretsDir);
    
    secretFiles.forEach(file => {
      try {
        const secretPath = path.join(secretsDir, file);
        const secretValue = fs.readFileSync(secretPath, 'utf8').trim();
        this.secrets.set(file.toLowerCase(), secretValue);
      } catch (error) {
        console.warn(`Failed to load secret from file ${file}:`, error.message);
      }
    });
  }

  get(secretName) {
    return this.secrets.get(secretName.toLowerCase());
  }

  has(secretName) {
    return this.secrets.has(secretName.toLowerCase());
  }

  // Mask secrets for logging
  mask(secretName) {
    const secret = this.get(secretName);
    if (!secret) return null;
    
    if (secret.length <= 8) {
      return '*'.repeat(secret.length);
    }
    
    return secret.substring(0, 4) + '*'.repeat(secret.length - 8) + secret.substring(secret.length - 4);
  }
}

module.exports = SecretsManager;

Configuration Testing

javascript
// tests/config.test.js
const config = require('../config');

describe('Configuration', () => {
  test('should have required properties', () => {
    expect(config.app).toBeDefined();
    expect(config.database).toBeDefined();
    expect(config.jwt).toBeDefined();
  });

  test('should have valid port number', () => {
    expect(config.app.port).toBeGreaterThan(0);
    expect(config.app.port).toBeLessThan(65536);
  });

  test('should have secure JWT secret in production', () => {
    if (config.app.env === 'production') {
      expect(config.jwt.secret).toBeDefined();
      expect(config.jwt.secret.length).toBeGreaterThanOrEqual(32);
    }
  });

  test('should have valid database configuration', () => {
    expect(config.database.url).toBeDefined();
    expect(config.database.host).toBeDefined();
    expect(config.database.port).toBeGreaterThan(0);
  });

  test('should have valid email configuration', () => {
    expect(config.email.smtp.host).toBeDefined();
    expect(config.email.smtp.port).toBeGreaterThan(0);
    expect(config.email.from.address).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });
});

Next Steps

In the next chapter, we'll explore core Node.js concepts including event-driven programming, streams, and asynchronous patterns.

Practice Exercises

  1. Create a configuration system for a multi-tenant application
  2. Implement a feature flag system with database persistence
  3. Build a secrets manager that integrates with cloud services
  4. Create configuration validation for a microservices architecture

Key Takeaways

  • Use environment variables for configuration that changes between deployments
  • Validate configuration to catch errors early
  • Separate secrets from regular configuration
  • Use feature flags for gradual rollouts and A/B testing
  • Implement configuration hot-reloading for dynamic updates
  • Test your configuration to ensure it works in all environments
  • Follow the principle of least privilege for secrets access

Content is for learning and research only.