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 typesLayered 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.*.local1// 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.