Environment Variables and Configuration
Environment variables enable configuration of applications across different environments. Learn secure configuration management practices for Node.js applications.
Challenge
Section titled “Challenge”Implement secure, flexible configuration management that adapts to different environments and protects sensitive information.
Explanation
Section titled “Explanation”Configuration Sources
Section titled “Configuration Sources”Environment Variables: Runtime configuration Configuration Files: Structured configuration Command Line Args: Runtime overrides Secrets Managers: Secure credential storage Configuration Servers: Centralized management
Best Practices
Section titled “Best Practices”- Never hardcode credentials
- Use different configs per environment
- Validate configuration on startup
- Document all configuration options
- Implement graceful defaults
Code Examples
Section titled “Code Examples”1. dotenv Configuration
Section titled “1. dotenv Configuration”require('dotenv').config();
module.exports = { app: { name: process.env.APP_NAME || 'MyApp', env: process.env.NODE_ENV || 'development', port: parseInt(process.env.PORT || '3000'), debug: process.env.DEBUG === 'true', }, database: { url: process.env.DATABASE_URL, pool: { min: parseInt(process.env.DB_POOL_MIN || '2'), max: parseInt(process.env.DB_POOL_MAX || '10'), }, timeout: parseInt(process.env.DB_TIMEOUT || '5000'), }, redis: { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, db: parseInt(process.env.REDIS_DB || '0'), }, jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN || '24h', }, email: { provider: process.env.EMAIL_PROVIDER || 'sendgrid', apiKey: process.env.EMAIL_API_KEY, from: process.env.EMAIL_FROM, },};2. Environment Validation
Section titled “2. Environment Validation”const Joi = require('joi');
const envSchema = Joi.object({ // App NODE_ENV: Joi.string() .valid('development', 'test', 'production') .default('development'), APP_NAME: Joi.string().required(), PORT: Joi.number().default(3000), DEBUG: Joi.boolean().default(false),
// Database DATABASE_URL: Joi.string().required(), DB_POOL_MIN: Joi.number().default(2), DB_POOL_MAX: Joi.number().default(10),
// Security JWT_SECRET: Joi.string().required().min(32), API_KEY: Joi.string().required(),
// Email EMAIL_PROVIDER: Joi.string().valid('sendgrid', 'mailgun'), EMAIL_API_KEY: Joi.string().required(),
// Optional services REDIS_HOST: Joi.string(), REDIS_PORT: Joi.number(),}).unknown().required();
function validateConfig(env) { const { error, value } = envSchema.validate(env, { allowUnknown: true, stripUnknown: true, });
if (error) { throw new Error(`Configuration validation error: ${error.message}`); }
return value;}
const validatedEnv = validateConfig(process.env);
module.exports = validatedEnv;3. Multi-Environment Configuration
Section titled “3. Multi-Environment Configuration”module.exports = { app: { name: 'MyApp', debug: false, }, database: { pool: { min: 2, max: 10 }, },};
// config/development.jsmodule.exports = { app: { debug: true, }, database: { url: 'postgresql://localhost/myapp_dev', },};
// config/production.jsmodule.exports = { app: { debug: false, }, database: { url: process.env.DATABASE_URL, pool: { min: 5, max: 30 }, },};
// config/index.jsconst config = require(`./default`);const envConfig = require(`./${process.env.NODE_ENV || 'development'}`);
module.exports = { ...config, ...envConfig };4. Secure Secrets Management
Section titled “4. Secure Secrets Management”const AWS = require('aws-sdk');const client = new AWS.SecretsManager();
class SecretsManager { constructor() { this.cache = new Map(); }
async getSecret(secretName) { // Check cache if (this.cache.has(secretName)) { return this.cache.get(secretName); }
try { const result = await client.getSecretValue({ SecretId: secretName }).promise();
let secret; if ('SecretString' in result) { secret = JSON.parse(result.SecretString); } else { secret = Buffer.from(result.SecretBinary, 'base64').toString('ascii'); }
// Cache for 1 hour this.cache.set(secretName, secret); setTimeout(() => this.cache.delete(secretName), 3600000);
return secret; } catch (error) { throw new Error(`Failed to retrieve secret ${secretName}: ${error.message}`); } }
clearCache(secretName) { this.cache.delete(secretName); }}
module.exports = new SecretsManager();5. Runtime Configuration Updates
Section titled “5. Runtime Configuration Updates”class ConfigurationService { constructor(baseConfig) { this.config = { ...baseConfig }; this.watchers = []; }
get(key) { return this.getNestedValue(this.config, key); }
set(key, value) { this.setNestedValue(this.config, key, value); this.notifyWatchers(key, value); }
subscribe(key, callback) { this.watchers.push({ key, callback }); }
notifyWatchers(key, value) { this.watchers.forEach(watcher => { if (key.startsWith(watcher.key)) { watcher.callback(value, key); } }); }
getNestedValue(obj, path) { return path.split('.').reduce((current, prop) => current?.[prop], obj); }
setNestedValue(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); const target = keys.reduce((current, prop) => { current[prop] = current[prop] || {}; return current[prop]; }, obj); target[lastKey] = value; }}
// Usageconst configService = new ConfigurationService(require('./config'));
configService.subscribe('database.url', (value) => { console.log('Database URL changed:', value); // Reconnect to new database});
configService.set('database.url', 'postgresql://newhost/db');6. Feature Flags Configuration
Section titled “6. Feature Flags Configuration”class FeatureFlagManager { constructor(config) { this.flags = config.flags || {}; this.overrides = new Map(); }
isEnabled(flagName, context = {}) { // Check overrides first if (this.overrides.has(flagName)) { return this.overrides.get(flagName); }
const flag = this.flags[flagName]; if (!flag) return false;
// Check conditions if (flag.enabled === false) return false; if (flag.enabled === true) return true;
// Percentage rollout if (flag.rolloutPercentage) { const hash = this.hashContext(context); return (hash % 100) < flag.rolloutPercentage; }
// User-based if (flag.enabledUsers && context.userId) { return flag.enabledUsers.includes(context.userId); }
return false; }
override(flagName, value) { this.overrides.set(flagName, value); }
clearOverride(flagName) { this.overrides.delete(flagName); }
hashContext(context) { const str = JSON.stringify(context); let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; // Convert to 32-bit integer } return Math.abs(hash); }}
// Usageconst featureFlags = new FeatureFlagManager({ flags: { newPaymentFlow: { enabled: true, rolloutPercentage: 50, }, betaFeature: { enabled: true, enabledUsers: ['user-1', 'user-2'], }, },});
if (featureFlags.isEnabled('newPaymentFlow', { userId: 'user-123' })) { // Use new payment flow}Best Practices
Section titled “Best Practices”-
Security
- Never commit secrets to version control
- Use .gitignore for .env files
- Rotate secrets regularly
- Use encrypted storage for sensitive data
-
Organization
- Group related settings
- Use consistent naming conventions
- Document all configuration options
- Provide sensible defaults
-
Validation
- Validate config on startup
- Fail fast on invalid configuration
- Type-check values
- Test configuration changes
-
Flexibility
- Support multiple sources
- Allow environment overrides
- Enable feature flags
- Support dynamic updates
-
Monitoring
- Log configuration changes
- Alert on invalid values
- Track configuration versions
- Audit access to sensitive config
Real-World Applications
Section titled “Real-World Applications”- Multi-tenant SaaS: Per-tenant configuration
- CI/CD Pipelines: Environment-specific builds
- Microservices: Distributed configuration
- Feature Rollout: Gradual feature enablement
- Secrets Management: Centralized credential storage