Configuration Patterns
Well-structured configuration is crucial for building maintainable Node.js applications. In this lesson, you'll learn patterns for managing configuration across different environments, validating settings, and organizing complex configurations.
Why Configuration Patterns Matter
Configuration affects how your application behaves:
- Database connections: Different databases for dev/prod
- API endpoints: Staging vs production URLs
- Feature flags: Enable/disable features
- Security settings: Secrets, tokens, keys
- Performance tuning: Cache TTL, pool sizes
Good configuration patterns help you:
- Avoid hardcoding values
- Switch environments easily
- Keep secrets secure
- Validate settings early
Basic Configuration Object
Start with a simple configuration object:
const config = {
port: process.env.PORT || 3000,
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
name: process.env.DB_NAME || 'myapp'
},
api: {
timeout: parseInt(process.env.API_TIMEOUT) || 5000,
retries: parseInt(process.env.API_RETRIES) || 3
}
};
module.exports = config;
Environment-Based Configuration
Create different configurations for each environment:
// config/index.js
const development = require('./development');
const production = require('./production');
const test = require('./test');
const configs = {
development,
production,
test
};
const env = process.env.NODE_ENV || 'development';
module.exports = configs[env];
// config/development.js
module.exports = {
port: 3000,
database: {
host: 'localhost',
name: 'app_dev'
},
debug: true,
logging: { level: 'debug' }
};
// config/production.js
module.exports = {
port: process.env.PORT,
database: {
host: process.env.DB_HOST,
name: process.env.DB_NAME
},
debug: false,
logging: { level: 'error' }
};
Merging Configurations
Combine base configuration with environment-specific overrides:
const baseConfig = {
app: { name: 'MyApp', version: '1.0.0' },
server: { port: 3000, host: 'localhost' },
database: { poolSize: 5, timeout: 5000 },
features: { analytics: true, notifications: true }
};
const productionOverrides = {
server: { port: 8080, host: '0.0.0.0' },
database: { poolSize: 20 }
};
// Deep merge function
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(target[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
const config = deepMerge(baseConfig, productionOverrides);
Configuration Validation
Validate configuration at startup to catch errors early:
function validateConfig(config) {
const errors = [];
// Required fields
if (!config.database?.host) {
errors.push('database.host is required');
}
// Type validation
if (typeof config.port !== 'number' || config.port < 1 || config.port > 65535) {
errors.push('port must be a number between 1 and 65535');
}
// Enum validation
const validLogLevels = ['debug', 'info', 'warn', 'error'];
if (!validLogLevels.includes(config.logging?.level)) {
errors.push(`logging.level must be one of: ${validLogLevels.join(', ')}`);
}
if (errors.length > 0) {
throw new Error(`Configuration errors:\n${errors.join('\n')}`);
}
return config;
}
Configuration File Patterns
Single Config File
// config.js
require('dotenv').config();
module.exports = {
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT) || 3000,
database: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE) || 5
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379'
},
auth: {
jwtSecret: process.env.JWT_SECRET,
tokenExpiry: process.env.TOKEN_EXPIRY || '24h'
}
};
Config Factory
// config/index.js
function createConfig() {
const env = process.env.NODE_ENV || 'development';
const base = {
env,
isProduction: env === 'production',
isDevelopment: env === 'development',
isTest: env === 'test'
};
const envConfig = require(`./${env}`);
return { ...base, ...envConfig };
}
module.exports = createConfig();
Freezing Configuration
Prevent accidental modification of configuration:
function freezeConfig(config) {
Object.freeze(config);
Object.keys(config).forEach(key => {
if (typeof config[key] === 'object' && config[key] !== null) {
freezeConfig(config[key]);
}
});
return config;
}
const config = freezeConfig({
port: 3000,
database: { host: 'localhost' }
});
// This will silently fail (or throw in strict mode)
config.port = 8080;
console.log(config.port); // Still 3000
Configuration Providers
Abstract configuration sources for flexibility:
class ConfigProvider {
constructor() {
this.sources = [];
}
addSource(source) {
this.sources.push(source);
return this;
}
get(key, defaultValue) {
for (const source of this.sources) {
const value = source.get(key);
if (value !== undefined) return value;
}
return defaultValue;
}
}
// Environment source
class EnvSource {
get(key) {
return process.env[key];
}
}
// File source
class FileSource {
constructor(config) {
this.config = config;
}
get(key) {
return this.config[key];
}
}
// Usage
const config = new ConfigProvider()
.addSource(new EnvSource())
.addSource(new FileSource({ PORT: '3000' }));
const port = config.get('PORT', '8080');
Secret Management
Handle sensitive configuration securely:
// Don't log secrets
function getSafeConfig(config) {
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'apiKey'];
function mask(obj, path = '') {
const result = {};
for (const [key, value] of Object.entries(obj)) {
const isSensitive = sensitiveKeys.some(s =>
key.toLowerCase().includes(s)
);
if (isSensitive) {
result[key] = '***REDACTED***';
} else if (typeof value === 'object' && value !== null) {
result[key] = mask(value, `${path}.${key}`);
} else {
result[key] = value;
}
}
return result;
}
return mask(config);
}
Common Configuration Patterns Summary
| Pattern | Use Case | Example |
|---|---|---|
| Environment Variables | Secrets, deployment config | process.env.API_KEY |
| Config Files | Structured settings | config/production.js |
| Deep Merge | Base + overrides | Combine base with env-specific |
| Validation | Catch errors early | Required fields, types |
| Freezing | Prevent modification | Object.freeze(config) |
| Redaction | Safe logging | Hide passwords in logs |
Key Takeaways
- Separate configuration from code using environment variables
- Use environment-specific configuration files
- Merge base configuration with environment overrides
- Validate configuration at startup
- Freeze configuration to prevent accidental changes
- Never log sensitive configuration values
- Provide sensible defaults for optional settings
- Document all configuration options
Summary
Configuration management is essential for building production-ready applications. You've learned how to structure configuration for different environments, merge and validate settings, and handle sensitive data securely. These patterns will help you build applications that are easy to deploy and maintain across development, staging, and production environments.
Next, you'll dive into asynchronous patterns in Node.js, starting with callbacks.

