Skip to main content

FileConfigProvider

File-based configuration supporting JSON and YAML with hot reload and multi-file merging.

Installation

npm install @stratix/config-file

Basic Usage

import { FileConfigProvider } from '@stratix/config-file';

const config = new FileConfigProvider({
files: ['./config/default.json'],
});

// Get values
const port = await config.getRequired<number>('server.port');
const host = await config.get<string>('server.host', 'localhost');

Configuration Options

interface FileConfigProviderOptions {
// Array of file paths (in merge order)
files: string[];

// Skip missing files instead of throwing
optional?: boolean; // default: false

// Validation schema
schema?: ConfigSchema;

// Enable hot reload (watches for file changes)
watch?: boolean; // default: false
}

Supported Formats

JSON

// config/default.json
{
"server": {
"port": 3000,
"host": "localhost"
},
"database": {
"url": "postgresql://localhost/mydb",
"poolSize": 10
},
"features": {
"enableCache": true,
"enableMetrics": false
}
}

YAML

# config/default.yaml
server:
port: 3000
host: localhost

database:
url: postgresql://localhost/mydb
poolSize: 10

features:
enableCache: true
enableMetrics: false

Multi-File Configuration

Environment-Specific Files

const nodeEnv = process.env.NODE_ENV || 'development';

const config = new FileConfigProvider({
files: [
'./config/default.json', // Base configuration
`./config/${nodeEnv}.json`, // Environment-specific
'./config/local.json', // Local overrides (gitignored)
],
optional: true, // Skip local.json if it doesn't exist
});

File Structure

config/
├── default.json # Base configuration
├── development.json # Development overrides
├── staging.json # Staging overrides
├── production.json # Production overrides
├── test.json # Test configuration
└── local.json # Local overrides (gitignored)

Example Files

default.json - Base configuration:

{
"server": {
"port": 3000,
"host": "localhost",
"cors": {
"enabled": true,
"origins": ["http://localhost:3000"]
}
},
"database": {
"url": "postgresql://localhost:5432/devdb",
"poolSize": 10,
"ssl": false
},
"logging": {
"level": "info",
"format": "json"
}
}

production.json - Production overrides:

{
"server": {
"port": 8080,
"host": "0.0.0.0",
"cors": {
"origins": ["https://myapp.com"]
}
},
"database": {
"poolSize": 50,
"ssl": true
},
"logging": {
"level": "warn"
}
}

Merge Behavior

Later files override earlier ones:

const config = new FileConfigProvider({
files: ['default.json', 'production.json'],
});

const result = await config.getAll();
// {
// server: {
// port: 8080, // from production.json
// host: '0.0.0.0', // from production.json
// cors: {
// enabled: true, // from default.json (not overridden)
// origins: ['https://myapp.com'] // from production.json
// }
// },
// database: {
// url: 'postgresql://localhost:5432/devdb', // from default.json
// poolSize: 50, // from production.json
// ssl: true // from production.json
// },
// logging: {
// level: 'warn', // from production.json
// format: 'json' // from default.json
// }
// }

Hot Reload

Enable Watch Mode

const config = new FileConfigProvider({
files: ['./config.json'],
watch: true, // Enable hot reload
});

// Listen for changes
const unwatch = config.watch((changes) => {
for (const change of changes) {
console.log(`Config changed: ${change.key}`);
console.log(` Old: ${change.oldValue}`);
console.log(` New: ${change.newValue}`);
}
});

// Later: stop watching
unwatch();

React to Changes

const config = new FileConfigProvider({
files: ['./config/development.json'],
watch: true,
});

config.watch?.((changes) => {
for (const change of changes) {
// React to specific changes
if (change.key === 'logging.level') {
logger.setLevel(change.newValue as LogLevel);
console.log(`Log level updated to: ${change.newValue}`);
}

if (change.key === 'features.enableMetrics') {
if (change.newValue) {
metricsService.start();
} else {
metricsService.stop();
}
}
}
});

Validation

With Zod

import { z } from 'zod';
import { FileConfigProvider } from '@stratix/config-file';

const ConfigSchema = z.object({
server: z.object({
port: z.number().int().min(1000).max(65535),
host: z.string(),
}),
database: z.object({
url: z.string().url(),
poolSize: z.number().int().positive(),
}),
});

const config = new FileConfigProvider({
files: ['./config.json'],
schema: {
async validate(data) {
const result = ConfigSchema.safeParse(data);
if (result.success) {
return { success: true, data: result.data };
}
return {
success: false,
errors: result.error.errors.map(e => ({
path: e.path.join('.'),
message: e.message,
})),
};
},
},
});

// Will throw if validation fails
const validatedConfig = await config.getAll();

Accessing Configuration

Get Individual Values

const port = await config.get<number>('server.port');
const dbUrl = await config.get<string>('database.url');

Get Namespaces

const serverConfig = await config.getNamespace('server');
// { port: 3000, host: 'localhost', cors: {...} }

const dbConfig = await config.getNamespace('database');
// { url: '...', poolSize: 10, ssl: false }

Get All Configuration

interface AppConfig {
server: ServerConfig;
database: DatabaseConfig;
features: FeatureFlags;
}

const config = await provider.getAll<AppConfig>();

Required vs Optional

// Throws if not found
const apiKey = await config.getRequired('api.key');

// Returns undefined or default
const timeout = await config.get('api.timeout', 5000);

Integration with ApplicationBuilder

import { ApplicationBuilder } from '@stratix/runtime';
import { FileConfigProvider } from '@stratix/config-file';

const nodeEnv = process.env.NODE_ENV || 'development';

const config = new FileConfigProvider({
files: [
'./config/default.json',
`./config/${nodeEnv}.json`,
],
watch: nodeEnv === 'development', // Hot reload in dev only
});

const app = await ApplicationBuilder.create()
.useConfig(config)
.useContainer(container)
.useLogger(logger)
.build();

Common Patterns

Docker Configuration

// Mount config directory as volume
const config = new FileConfigProvider({
files: ['/app/config/production.json'],
optional: false,
});

docker-compose.yml:

services:
app:
volumes:
- ./config:/app/config

Kubernetes ConfigMaps

apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
config.json: |
{
"server": {
"port": 8080
}
}

Mount and use:

const config = new FileConfigProvider({
files: ['/etc/config/config.json'],
});

Development vs Production

const isDevelopment = process.env.NODE_ENV === 'development';

const config = new FileConfigProvider({
files: [
'./config/default.json',
isDevelopment
? './config/development.json'
: './config/production.json',
],
watch: isDevelopment, // Hot reload only in dev
});

Best Practices

1. Use Environment-Specific Files

config/
├── default.json # Always loaded first
├── development.json # Dev overrides
├── production.json # Prod overrides
└── local.json # Local overrides (gitignored)

2. Validate Configuration Early

async function bootstrap() {
try {
const config = await configProvider.getAll();
console.log('Configuration loaded and validated');
} catch (error) {
console.error('Configuration error:', error);
process.exit(1);
}
}

3. Use .gitignore

# .gitignore
config/local.json
config/secrets.json
*.env

4. Document Configuration

Create config/README.md:

# Configuration Files

- `default.json` - Base configuration
- `development.json` - Development overrides
- `production.json` - Production overrides
- `local.json` - Local overrides (gitignored)

## Required Values

- `server.port` - HTTP port
- `database.url` - Database connection string

5. Use Type-Safe Interfaces

interface AppConfig {
server: {
port: number;
host: string;
};
database: {
url: string;
poolSize: number;
};
}

const config = await provider.getAll<AppConfig>();
// Fully typed access
config.server.port; // number

Troubleshooting

File Not Found

// Use optional for files that may not exist
const config = new FileConfigProvider({
files: [
'./config/default.json', // Must exist
'./config/local.json', // May not exist
],
optional: true, // Don't fail if local.json missing
});

Unsupported Format

Only JSON and YAML are supported:

// ✅ Supported
files: ['config.json', 'config.yaml', 'config.yml']

// ❌ Not supported
files: ['config.toml', 'config.ini']

Watch Not Working

Ensure watch is enabled:

const config = new FileConfigProvider({
files: ['./config.json'],
watch: true, // ← Enable watching
});

// Subscribe to changes
config.watch?.((changes) => {
console.log('Changes:', changes);
});

Manual Reload

// Manually trigger reload
await config.reload();

// Useful after external config updates

API Reference

Methods

get<T>(key: string, defaultValue?: T): Promise<T | undefined>

Get configuration value with optional default.

getRequired<T>(key: string): Promise<T>

Get required configuration value. Throws if not found.

getAll<T>(): Promise<T>

Get all configuration as object.

getNamespace<T>(namespace: string): Promise<T>

Get configuration for specific namespace.

has(key: string): Promise<boolean>

Check if configuration key exists.

reload(): Promise<void>

Manually reload configuration files.

watch(callback): () => void

Watch for configuration changes. Returns unsubscribe function.

Next Steps