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 dotenvCreate 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=100bash
# .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=50Loading 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 joijavascript
// 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
- Create a configuration system for a multi-tenant application
- Implement a feature flag system with database persistence
- Build a secrets manager that integrates with cloud services
- 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