@bernierllc/config-manager
v1.0.5
Published
Environment-based configuration management with validation, hot reloading, and multi-source support
Readme
@bernierllc/config-manager
Environment-based configuration management with validation, hot reloading, and multi-source support.
Features
- 🔄 Multiple Sources - Environment variables, JSON/YAML files, remote APIs, CLI arguments
- 🔍 Type Safety - TypeScript interfaces with runtime validation
- ⚡ Hot Reloading - Automatic config updates without restart
- 📊 Hierarchical Merging - Override configs by priority
- ✅ Validation - Schema validation with detailed error messages
- 🔒 Secret Management - Secure handling of sensitive configuration
- 💾 Caching - Efficient config caching with TTL
- 🎛️ Specialized Managers - Database and API-specific configuration helpers
Installation
npm install @bernierllc/config-managerQuick Start
import {
ConfigManager,
EnvConfigSource,
FileConfigSource
} from '@bernierllc/config-manager';
interface AppConfig {
server: {
port: number;
host: string;
};
database: {
url: string;
maxConnections: number;
};
auth: {
jwtSecret: string;
tokenExpiry: string;
};
features: {
enableAnalytics: boolean;
enableCache: boolean;
};
}
const configManager = new ConfigManager<AppConfig>({
sources: [
new FileConfigSource('./config/default.json', { priority: 10 }),
new FileConfigSource(`./config/${process.env.NODE_ENV}.json`, {
priority: 20,
optional: true
}),
new EnvConfigSource({ prefix: 'APP_', priority: 30 })
],
defaults: {
server: {
port: 3000,
host: '0.0.0.0'
},
features: {
enableAnalytics: false,
enableCache: true
}
},
required: ['database.url', 'auth.jwtSecret'],
watch: true
});
const config = await configManager.load();
console.log(`Server starting on ${config.server.host}:${config.server.port}`);
// Get specific values
const port = configManager.get('server').port;
const jwtSecret = configManager.get('auth').jwtSecret;
// Check if key exists
if (configManager.has('features')) {
const features = configManager.get('features');
}
// Update configuration dynamically
configManager.set('server', { ...config.server, port: 8080 });
configManager.merge({ features: { enableAnalytics: true } });Configuration Sources
Environment Variables
import { EnvConfigSource } from '@bernierllc/config-manager';
// Basic environment source
const envSource = new EnvConfigSource();
// With prefix filtering
const appEnvSource = new EnvConfigSource({
prefix: 'APP_',
separator: '_'
});
// Environment variables like APP_DATABASE_HOST become config.database.hostFile Sources
import { FileConfigSource } from '@bernierllc/config-manager';
// JSON configuration
const jsonSource = new FileConfigSource('./config.json');
// YAML configuration
const yamlSource = new FileConfigSource('./config.yaml', {
optional: true, // Don't fail if file doesn't exist
priority: 20
});
// With transformation
const transformSource = new FileConfigSource('./config.json', {
transform: (data) => ({
...data,
port: parseInt(data.port) // Transform string to number
})
});Remote Configuration
import { RemoteConfigSource } from '@bernierllc/config-manager';
const remoteSource = new RemoteConfigSource('https://config.example.com/api/config', {
headers: {
'Authorization': `Bearer ${process.env.CONFIG_TOKEN}`
},
refreshInterval: 30000, // Refresh every 30 seconds
retries: 3,
fallbackData: {
feature: { enabled: false } // Used if remote fails
}
});Command Line Arguments
import { CliConfigSource } from '@bernierllc/config-manager';
// Parse process.argv
const cliSource = new CliConfigSource();
// With custom args and aliases
const customCli = new CliConfigSource({
aliases: { p: 'port', h: 'host' },
transform: {
timeout: (value) => parseInt(value) * 1000 // Convert to milliseconds
}
}, ['--port=3000', '-h', 'localhost']);Schema Validation
const configManager = new ConfigManager<AppConfig>({
sources: [envSource, fileSource],
schema: {
type: 'object',
properties: {
server: {
type: 'object',
properties: {
port: {
type: 'number',
validate: (port) => port > 0 && port < 65536,
description: 'Server port (1-65535)'
},
host: {
type: 'string',
default: 'localhost'
}
}
},
database: {
type: 'object',
properties: {
url: {
type: 'string',
required: true,
validate: (url) => url.startsWith('postgresql://'),
description: 'PostgreSQL connection URL'
},
maxConnections: {
type: 'number',
default: 10,
validate: (n) => n > 0 && n <= 100
}
}
}
}
},
required: ['database.url'],
onUpdate: (newConfig, prevConfig) => {
console.log('Configuration updated');
},
onError: (error, sourceName) => {
console.error(`Config source '${sourceName}' failed:`, error);
}
});
// Validate configuration
const validation = configManager.validate();
if (!validation.valid) {
validation.errors.forEach(error => {
console.error(`${error.path}: ${error.message}`);
});
}Hot Reloading
const configManager = new ConfigManager({
sources: [
new FileConfigSource('./config.json'),
new RemoteConfigSource('https://api.example.com/config', {
refreshInterval: 60000 // Check every minute
})
],
watch: true, // Enable hot reloading
onUpdate: async (newConfig, prevConfig) => {
// Handle configuration changes
if (newConfig.features.enableNewFeature !== prevConfig.features.enableNewFeature) {
if (newConfig.features.enableNewFeature) {
await enableNewFeature();
} else {
await disableNewFeature();
}
}
}
});
// Start watching for changes
configManager.startWatching();
// Stop watching when done
process.on('exit', () => {
configManager.stopWatching();
});Specialized Configuration Managers
Database Configuration
import { DatabaseConfig, EnvConfigSource } from '@bernierllc/config-manager';
const dbConfig = new DatabaseConfig({
sources: [new EnvConfigSource({ prefix: 'DB_' })],
schema: {
type: 'object',
properties: {
host: { type: 'string', required: true },
port: { type: 'number', default: 5432 },
database: { type: 'string', required: true },
username: { type: 'string', required: true },
password: { type: 'string', required: true },
ssl: { type: 'boolean', default: false },
pool: {
type: 'object',
properties: {
min: { type: 'number', default: 2 },
max: { type: 'number', default: 10 }
}
}
}
}
});
await dbConfig.load();
// Get connection string
const connectionString = dbConfig.getConnectionString();
// postgresql://user:pass@host:port/database
// Get pool configuration
const poolConfig = dbConfig.getPoolConfig();
// { min: 2, max: 10, acquireTimeoutMillis: 60000, ... }
// Get engine-specific config
const pgConfig = dbConfig.getEngineConfig('postgresql');API Configuration
import { ApiConfig, FileConfigSource } from '@bernierllc/config-manager';
const apiConfig = new ApiConfig({
sources: [new FileConfigSource('./api-config.json')]
});
await apiConfig.load();
// Get resolved endpoints (relative URLs combined with baseUrl)
const endpoints = apiConfig.getEndpoints();
// { users: 'https://api.example.com/users', posts: 'https://api.example.com/posts' }
// Get authentication config
const auth = apiConfig.getAuth();
// { type: 'bearer', token: 'abc123' }
// Get request configuration for HTTP client
const requestConfig = apiConfig.getRequestConfig();
// { timeout: 30000, retries: 3, headers: { Authorization: 'Bearer abc123' } }
// Test API connectivity
const results = await apiConfig.testConnectivity();
// [{ endpoint: 'users', success: true }, { endpoint: 'posts', success: false, error: 'timeout' }]Configuration Export and Debugging
// Export current configuration (with sensitive data redacted)
console.log('JSON:', configManager.export('json'));
console.log('YAML:', configManager.export('yaml'));
console.log('ENV:', configManager.export('env'));
// Debug configuration sources
const sourceInfo = configManager.getSourceInfo();
sourceInfo.forEach(info => {
console.log(`Source: ${info.name} (priority: ${info.priority})`);
console.log(` Status: ${info.loaded ? 'loaded' : 'failed'}`);
console.log(` Keys: ${info.keys.join(', ')}`);
if (info.error) {
console.log(` Error: ${info.error}`);
}
});Advanced Usage
Custom Configuration Sources
import { ConfigSource, ConfigData } from '@bernierllc/config-manager';
class CustomConfigSource implements ConfigSource {
name = 'custom';
priority = 50;
async load(): Promise<ConfigData> {
// Your custom loading logic
return { customKey: 'customValue' };
}
watch(callback: (data: ConfigData) => void): void {
// Optional: implement watching
setInterval(async () => {
const data = await this.load();
callback(data);
}, 10000);
}
stop(): void {
// Optional: cleanup watching
}
}Configuration Transforms
const configManager = new ConfigManager({
sources: [fileSource],
transform: {
// Transform relative URLs to absolute
'api.endpoints': (endpoints) => {
const baseUrl = configManager.get('api').baseUrl;
return Object.fromEntries(
Object.entries(endpoints).map(([key, url]) => [
key,
url.startsWith('http') ? url : `${baseUrl}${url}`
])
);
},
// Parse connection strings
'database.url': (url) => {
// Parse and validate database URL
return new URL(url);
}
}
});Partial Updates
// Validate partial configuration before applying
const partialConfig = { server: { port: 8080 } };
const validation = configManager.validatePartial(partialConfig);
if (validation.valid) {
configManager.merge(partialConfig);
} else {
console.error('Invalid partial config:', validation.errors);
}Error Handling
const configManager = new ConfigManager({
sources: [
new RemoteConfigSource('https://unreliable-config-api.com'),
new FileConfigSource('./fallback-config.json') // Fallback
],
onError: (error, sourceName) => {
// Log error but continue with other sources
console.warn(`Config source '${sourceName}' failed: ${error.message}`);
// Could also emit metrics, send alerts, etc.
metrics.increment('config.source.error', { source: sourceName });
}
});
try {
const config = await configManager.load();
} catch (error) {
// All sources failed
console.error('Failed to load configuration:', error);
process.exit(1);
}Caching
const configManager = new ConfigManager({
sources: [remoteSource],
cache: true,
cacheTTL: 5 * 60 * 1000, // 5 minutes
onUpdate: (newConfig) => {
console.log('Configuration cache refreshed');
}
});
// First load - fetches from sources
const config1 = await configManager.load();
// Second load - uses cache (if within TTL)
const config2 = await configManager.load();
// Force reload - bypasses cache
const config3 = await configManager.reload();TypeScript Support
Full TypeScript support with type-safe configuration access:
interface MyConfig {
server: {
port: number;
host: string;
};
database: {
url: string;
};
}
const configManager = new ConfigManager<MyConfig>({
// ... sources
});
await configManager.load();
// Type-safe access
const port: number = configManager.get('server').port;
const host: string = configManager.get('server').host;
// TypeScript will catch invalid keys
// configManager.get('invalid'); // ✗ TypeScript errorPerformance
- Fast Configuration Access: < 1ms for cached configurations
- Efficient Merging: Optimized deep merge algorithms
- Smart Caching: TTL-based caching with automatic invalidation
- Minimal Memory Usage: Efficient data structures and cleanup
- Hot Reload Optimization: Debounced file watching and change detection
Integration Status
Logger Integration
Status: Optional (recommended for configuration monitoring)
Justification: While this package can function without logging, it's highly recommended to integrate @bernierllc/logger for configuration change monitoring and debugging. Config managers handle sensitive data and configuration changes, and logging these events helps with security auditing, debugging configuration issues, and tracking configuration drift. However, the package is designed to work without logging for environments where logging isn't available.
Pattern: Optional integration - package works without logger, but logger enhances configuration monitoring capabilities.
Example Integration:
import { Logger } from '@bernierllc/logger';
import { ConfigManager } from '@bernierllc/config-manager';
const logger = new Logger({ service: 'config-manager' });
const configManager = new ConfigManager({
// ... config
});
// Log configuration changes
configManager.on('change', (changes) => {
logger.info('Configuration changed', { keys: Object.keys(changes) });
});NeverHub Integration
Status: Optional (recommended for distributed configuration)
Justification: While this package can function without NeverHub, it's highly recommended to integrate @bernierllc/neverhub-adapter for distributed configuration management. Config managers often need to coordinate configuration across services, and NeverHub provides a service mesh for publishing configuration changes that other services can subscribe to. However, the package is designed to work without NeverHub for simpler use cases.
Pattern: Optional integration - package works without NeverHub, but NeverHub enhances distributed configuration capabilities.
Example Integration:
import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
import { ConfigManager } from '@bernierllc/config-manager';
const neverhub = new NeverHubAdapter({ service: 'config-manager' });
const configManager = new ConfigManager({ /* config */ });
// Publish configuration changes to NeverHub
configManager.on('change', async (changes) => {
await neverhub.publish('config.changed', {
service: 'config-manager',
changes,
timestamp: Date.now()
});
});Docs-Suite Integration
Status: Ready
Format: TypeDoc-compatible JSDoc comments are included throughout the source code. All public APIs are documented with examples and type information.
Security
- Sensitive Data Protection: Automatic redaction of passwords, keys, tokens
- Source Validation: Validate configuration sources and data integrity
- Secure Remote Fetching: Timeout protection and retry limits
- No Secret Logging: Sensitive values never appear in logs or exports
Dependencies
chokidar- File watching for hot reloadyaml- YAML configuration file supportdotenv- Environment file loading
License
Copyright (c) 2025 Bernier LLC. All rights reserved.
