Back to Blog
ConfigurationDevOpsBest PracticesEnvironment Variables

Configuration Management Best Practices

Manage application configuration properly. From environment variables to config files to feature flags.

B
Bootspring Team
Engineering
June 20, 2024
5 min read

Configuration determines how your application behaves across environments. Poor configuration management leads to security vulnerabilities, deployment failures, and debugging nightmares.

Configuration Sources#

Priority (highest to lowest): 1. Command-line arguments 2. Environment variables 3. Config files (environment-specific) 4. Config files (default) 5. Hardcoded defaults Rule: Higher priority sources override lower ones

Environment Variables#

Basic Setup#

1// config.ts 2import { z } from 'zod'; 3 4const envSchema = z.object({ 5 NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), 6 PORT: z.string().transform(Number).default('3000'), 7 DATABASE_URL: z.string().url(), 8 REDIS_URL: z.string().url().optional(), 9 JWT_SECRET: z.string().min(32), 10 API_KEY: z.string(), 11 LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'), 12}); 13 14// Validate and parse 15const parsed = envSchema.safeParse(process.env); 16 17if (!parsed.success) { 18 console.error('Invalid environment variables:', parsed.error.flatten()); 19 process.exit(1); 20} 21 22export const config = parsed.data;

Type-Safe Access#

1// Access configuration 2import { config } from './config'; 3 4const server = app.listen(config.PORT, () => { 5 console.log(`Server running on port ${config.PORT}`); 6}); 7 8// No more process.env throughout codebase 9// TypeScript knows the types

Layered Configuration#

1// config/default.ts 2export default { 3 app: { 4 name: 'MyApp', 5 port: 3000, 6 }, 7 database: { 8 poolSize: 10, 9 timeout: 5000, 10 }, 11 cache: { 12 ttl: 3600, 13 }, 14 features: { 15 newDashboard: false, 16 darkMode: true, 17 }, 18}; 19 20// config/development.ts 21export default { 22 database: { 23 poolSize: 5, 24 }, 25 features: { 26 newDashboard: true, // Enable in dev 27 }, 28}; 29 30// config/production.ts 31export default { 32 database: { 33 poolSize: 20, 34 timeout: 10000, 35 }, 36};
1// config/index.ts 2import defaultConfig from './default'; 3import developmentConfig from './development'; 4import productionConfig from './production'; 5 6type DeepPartial<T> = { 7 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; 8}; 9 10function deepMerge<T>(target: T, source: DeepPartial<T>): T { 11 const result = { ...target }; 12 13 for (const key of Object.keys(source) as Array<keyof T>) { 14 const sourceValue = source[key]; 15 const targetValue = target[key]; 16 17 if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) { 18 result[key] = deepMerge(targetValue as any, sourceValue as any); 19 } else if (sourceValue !== undefined) { 20 result[key] = sourceValue as T[keyof T]; 21 } 22 } 23 24 return result; 25} 26 27const envConfigs: Record<string, DeepPartial<typeof defaultConfig>> = { 28 development: developmentConfig, 29 production: productionConfig, 30}; 31 32const env = process.env.NODE_ENV || 'development'; 33const envConfig = envConfigs[env] || {}; 34 35export const config = deepMerge(defaultConfig, envConfig);

Feature Flags#

1// Feature flag service 2interface FeatureFlag { 3 name: string; 4 enabled: boolean; 5 rolloutPercentage?: number; 6 allowedUsers?: string[]; 7 allowedGroups?: string[]; 8} 9 10class FeatureFlagService { 11 private flags: Map<string, FeatureFlag> = new Map(); 12 13 async loadFlags() { 14 // Load from remote config service 15 const response = await fetch('/api/feature-flags'); 16 const flags: FeatureFlag[] = await response.json(); 17 18 for (const flag of flags) { 19 this.flags.set(flag.name, flag); 20 } 21 } 22 23 isEnabled(flagName: string, context?: { userId?: string; groups?: string[] }): boolean { 24 const flag = this.flags.get(flagName); 25 26 if (!flag) return false; 27 if (!flag.enabled) return false; 28 29 // Check user allowlist 30 if (flag.allowedUsers && context?.userId) { 31 if (flag.allowedUsers.includes(context.userId)) { 32 return true; 33 } 34 } 35 36 // Check group allowlist 37 if (flag.allowedGroups && context?.groups) { 38 if (flag.allowedGroups.some((g) => context.groups!.includes(g))) { 39 return true; 40 } 41 } 42 43 // Check rollout percentage 44 if (flag.rolloutPercentage !== undefined && context?.userId) { 45 const hash = this.hashUserId(context.userId); 46 return hash % 100 < flag.rolloutPercentage; 47 } 48 49 return flag.enabled; 50 } 51 52 private hashUserId(userId: string): number { 53 let hash = 0; 54 for (let i = 0; i < userId.length; i++) { 55 hash = (hash << 5) - hash + userId.charCodeAt(i); 56 hash |= 0; 57 } 58 return Math.abs(hash); 59 } 60} 61 62// Usage 63const featureFlags = new FeatureFlagService(); 64await featureFlags.loadFlags(); 65 66if (featureFlags.isEnabled('new-checkout', { userId: user.id })) { 67 return <NewCheckout />; 68} 69return <OldCheckout />;

Secrets Management#

1// Never commit secrets 2// .env.example (commit this) 3DATABASE_URL=postgres://user:pass@localhost:5432/db 4JWT_SECRET=your-secret-here-min-32-chars 5STRIPE_SECRET_KEY=sk_test_... 6 7// .gitignore 8.env 9.env.local 10.env.*.local
1// Secrets from cloud provider 2import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'; 3 4async function loadSecrets(): Promise<Record<string, string>> { 5 const client = new SecretsManagerClient({ region: 'us-east-1' }); 6 7 const command = new GetSecretValueCommand({ 8 SecretId: `${process.env.APP_NAME}/${process.env.NODE_ENV}`, 9 }); 10 11 const response = await client.send(command); 12 return JSON.parse(response.SecretString!); 13} 14 15// Load at startup 16const secrets = await loadSecrets(); 17const jwtSecret = secrets.JWT_SECRET;

Configuration Validation#

1// Validate configuration at startup 2function validateConfig(config: Config): void { 3 const errors: string[] = []; 4 5 // Required fields 6 if (!config.database.url) { 7 errors.push('DATABASE_URL is required'); 8 } 9 10 // Format validation 11 if (config.app.port < 1 || config.app.port > 65535) { 12 errors.push('PORT must be between 1 and 65535'); 13 } 14 15 // Environment-specific 16 if (config.env === 'production') { 17 if (!config.app.url.startsWith('https://')) { 18 errors.push('APP_URL must use HTTPS in production'); 19 } 20 21 if (config.database.ssl === false) { 22 errors.push('Database SSL must be enabled in production'); 23 } 24 } 25 26 if (errors.length > 0) { 27 console.error('Configuration errors:', errors); 28 process.exit(1); 29 } 30}

Runtime Configuration#

1// Hot-reload configuration 2class ConfigWatcher { 3 private config: Config; 4 private listeners: Array<(config: Config) => void> = []; 5 6 constructor(private configPath: string) { 7 this.config = this.loadConfig(); 8 this.watchForChanges(); 9 } 10 11 private loadConfig(): Config { 12 const content = fs.readFileSync(this.configPath, 'utf-8'); 13 return JSON.parse(content); 14 } 15 16 private watchForChanges() { 17 fs.watch(this.configPath, () => { 18 console.log('Config file changed, reloading...'); 19 this.config = this.loadConfig(); 20 this.listeners.forEach((listener) => listener(this.config)); 21 }); 22 } 23 24 get(): Config { 25 return this.config; 26 } 27 28 onChange(listener: (config: Config) => void) { 29 this.listeners.push(listener); 30 } 31} 32 33// Usage 34const configWatcher = new ConfigWatcher('./config.json'); 35 36configWatcher.onChange((config) => { 37 // Update log level without restart 38 logger.setLevel(config.logLevel); 39});

Best Practices#

DO: ✓ Validate configuration at startup ✓ Use typed configuration objects ✓ Separate secrets from config ✓ Use environment-specific configs ✓ Document all configuration options ✓ Provide sensible defaults DON'T: ✗ Hardcode configuration values ✗ Commit secrets to version control ✗ Use process.env throughout code ✗ Mix configuration with business logic ✗ Require restarts for all config changes

Conclusion#

Configuration management is infrastructure for your application. Invest in validation, type safety, and clear separation between environments.

Good configuration is invisible—it just works across all environments.

Share this article

Help spread the word about Bootspring