Best Practices
Production-ready patterns for configuration management in Stratix applications.
Configuration Strategy
12-Factor App Principles
Follow the 12-Factor App methodology:
- Store config in environment - Use ENV variables for deployment-specific config
- Strict separation - Never commit secrets or environment-specific config
- No grouping - Avoid environment-based config files in production
// Good: ENV-based configuration
const config = new EnvConfigProvider({
prefix: 'APP_',
});
// Avoid: Environment-specific files in production
const config = new FileConfigProvider({
files: [`./config/${process.env.NODE_ENV}.json`],
});
Configuration Layers
Use a layered approach with clear priorities:
const config = new CompositeConfigProvider({
providers: [
// 1. Environment variables (highest priority)
new EnvConfigProvider({ prefix: 'APP_' }),
// 2. Environment-specific files
new FileConfigProvider({
files: [`./config/${nodeEnv}.json`],
optional: true,
}),
// 3. Default configuration (lowest priority)
new FileConfigProvider({
files: ['./config/default.json'],
}),
],
strategy: 'first-wins',
});
Security
Separate Secrets from Config
Never mix secrets with public configuration:
import { EnvSecretsProvider } from '@stratix/secrets-env';
import { EnvConfigProvider } from '@stratix/config-env';
// Public configuration
const config = new EnvConfigProvider({
prefix: 'APP_',
});
// Sensitive data
const secrets = new EnvSecretsProvider({
prefix: 'SECRET_',
});
// Usage
const dbHost = await config.get('database.host'); // Public
const dbPassword = await secrets.get('database.password'); // Secret
Never Commit Secrets
Add to .gitignore:
# Environment files
.env
.env.local
.env.*.local
# Secret files
config/secrets.json
config/local.json
*.key
*.pem
Use Secret Management Services
In production, use dedicated secret management:
import { AwsSecretsProvider } from '@stratix/secrets-aws';
import { EnvConfigProvider } from '@stratix/config-env';
const isProd = process.env.NODE_ENV === 'production';
const secrets = isProd
? new AwsSecretsProvider({
region: 'us-east-1',
secretName: 'myapp/production',
})
: new EnvSecretsProvider({ prefix: 'SECRET_' });
const config = new EnvConfigProvider({ prefix: 'APP_' });
Validate Secrets
Ensure secrets meet security requirements:
import { z } from 'zod';
const SecretsSchema = z.object({
apiKey: z.string()
.min(32, 'API key must be at least 32 characters')
.regex(/^[A-Za-z0-9]+$/, 'API key contains invalid characters'),
jwtSecret: z.string()
.min(32, 'JWT secret must be at least 32 characters'),
databasePassword: z.string()
.min(16, 'Database password must be at least 16 characters')
.refine(
(pwd) => /[A-Z]/.test(pwd) && /[a-z]/.test(pwd) && /[0-9]/.test(pwd),
'Password must contain uppercase, lowercase, and numbers'
),
});
Validation
Fail Fast
Validate configuration at startup:
async function bootstrap() {
try {
// Validate all configuration
await config.getAll();
console.log('Configuration validated successfully');
} catch (error) {
console.error('Configuration validation failed:', error);
process.exit(1);
}
// Start application
const app = await ApplicationBuilder.create()
.useConfig(config)
.build();
await app.start();
}
Use Schemas
Always use schema validation in production:
import { z } from 'zod';
const AppConfigSchema = z.object({
nodeEnv: z.enum(['development', 'staging', 'production']),
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(),
ssl: z.boolean(),
}).refine(
(db) => {
// Require SSL in production
const env = process.env.NODE_ENV;
return env !== 'production' || db.ssl === true;
},
{ message: 'SSL is required in production' }
),
});
const config = new EnvConfigProvider({
prefix: 'APP_',
schema: {
async validate(data) {
const result = AppConfigSchema.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,
})),
};
},
},
});
Type Safety
Use TypeScript for type-safe configuration access:
interface AppConfig {
server: {
port: number;
host: string;
};
database: {
url: string;
poolSize: number;
};
}
const config = await provider.getAll<AppConfig>();
// Fully typed access
const port: number = config.server.port;
Environment Management
Use .env Files for Development
# .env.development
APP_PORT=3000
APP_HOST=localhost
APP_NODE_ENV=development
APP_DATABASE__URL=postgresql://localhost:5432/devdb
APP_DATABASE__POOL__SIZE=5
const config = new EnvConfigProvider({
prefix: 'APP_',
loadDotenv: true,
dotenvPath: `.env.${process.env.NODE_ENV || 'development'}`,
});
Document Required Variables
Create .env.example:
# .env.example
# Required environment variables
APP_PORT=3000
APP_HOST=localhost
APP_NODE_ENV=development
# Database configuration
APP_DATABASE__URL=postgresql://localhost:5432/mydb
APP_DATABASE__POOL__SIZE=10
APP_DATABASE__SSL=false
# Feature flags
APP_FEATURES__ENABLE_CACHE=true
APP_FEATURES__ENABLE_METRICS=false
Environment-Specific Defaults
const getDefaultConfig = () => {
const nodeEnv = process.env.NODE_ENV || 'development';
const defaults = {
development: {
database: { poolSize: 5, ssl: false },
logging: { level: 'debug' },
},
staging: {
database: { poolSize: 20, ssl: true },
logging: { level: 'info' },
},
production: {
database: { poolSize: 50, ssl: true },
logging: { level: 'warn' },
},
};
return defaults[nodeEnv] || defaults.development;
};
Performance
Enable Caching
Cache frequently accessed values:
const config = new EnvConfigProvider({
prefix: 'APP_',
cache: true, // Enable caching
cacheTTL: 300000, // 5 minutes
});
Load Configuration Once
Don't reload configuration unnecessarily:
// Good: Load once at startup
const appConfig = await config.getAll();
class UserService {
constructor(private readonly config: AppConfig) {}
async createUser() {
const maxUsers = this.config.limits.maxUsers;
// Use cached config
}
}
// Avoid: Loading on every request
class UserService {
async createUser() {
const maxUsers = await config.get('limits.maxUsers'); // Slow
}
}
Use Namespaces
Get entire namespaces instead of individual keys:
// Good: One call
const dbConfig = await config.getNamespace('database');
const { host, port, username, password } = dbConfig;
// Avoid: Multiple calls
const host = await config.get('database.host');
const port = await config.get('database.port');
const username = await config.get('database.username');
const password = await config.get('database.password');
Error Handling
Provide Defaults
Use sensible defaults for optional configuration:
const timeout = await config.get('api.timeout', 5000);
const retries = await config.get('api.retries', 3);
const enableMetrics = await config.get('features.enableMetrics', false);
Required vs Optional
Be explicit about required values:
// Required: Will throw if not found
const apiKey = await config.getRequired<string>('api.key');
const databaseUrl = await config.getRequired<string>('database.url');
// Optional: Returns undefined or default
const cacheTTL = await config.get<number>('cache.ttl', 300);
Graceful Degradation
async function initializeCache() {
try {
const cacheUrl = await config.get('cache.url');
if (cacheUrl) {
return new RedisCache(cacheUrl);
}
} catch (error) {
console.warn('Cache configuration not found, using in-memory cache');
}
return new InMemoryCache();
}
Detailed Error Messages
try {
await config.getAll();
} catch (error) {
if (error instanceof ConfigValidationError) {
console.error('Configuration validation failed:');
for (const err of error.errors) {
console.error(` - ${err.path}: ${err.message}`);
}
// Provide guidance
console.error('\nPlease check:');
console.error(' 1. Environment variables are set correctly');
console.error(' 2. Configuration files exist and are valid');
console.error(' 3. See .env.example for required variables');
}
process.exit(1);
}
Testing
Mock Configuration
import { describe, it, expect } from 'vitest';
describe('UserService', () => {
it('should enforce user limit from config', async () => {
const config = new EnvConfigProvider({
env: {
'APP_LIMITS__MAX_USERS': '100',
},
prefix: 'APP_',
loadDotenv: false,
});
const service = new UserService(config);
// Test with this config
});
});
Test Configuration Validation
describe('Config Validation', () => {
it('should reject invalid port', async () => {
const config = new EnvConfigProvider({
env: { 'APP_PORT': '-1' },
prefix: 'APP_',
loadDotenv: false,
schema: AppConfigSchema,
});
await expect(config.getAll()).rejects.toThrow(ConfigValidationError);
});
it('should require SSL in production', async () => {
const config = new EnvConfigProvider({
env: {
'APP_NODE_ENV': 'production',
'APP_DATABASE__SSL': 'false',
},
prefix: 'APP_',
loadDotenv: false,
schema: AppConfigSchema,
});
await expect(config.getAll()).rejects.toThrow('SSL is required');
});
});
Documentation
Document Configuration
Create docs/configuration.md:
# Configuration
## Environment Variables
### Required
- `APP_PORT` - HTTP server port (1000-65535)
- `APP_DATABASE__URL` - Database connection string
- `SECRET_API_KEY` - API authentication key
### Optional
- `APP_HOST` - HTTP server host (default: localhost)
- `APP_DATABASE__POOL__SIZE` - Connection pool size (default: 10)
- `APP_FEATURES__ENABLE_CACHE` - Enable caching (default: false)
## Example
See `.env.example` for a complete example.
## Validation
Configuration is validated at startup. See errors in console if validation fails.
README Instructions
Include configuration setup in your README:
## Configuration
1. Copy `.env.example` to `.env`:
```bash
cp .env.example .env
-
Edit
.envwith your settings -
Required variables:
APP_DATABASE__URL- Database connectionSECRET_API_KEY- API key (min 32 characters)
-
Start the application:
npm start
## Container Deployment
### Docker
Use environment variables:
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
# Don't copy .env files
# Configuration comes from docker-compose or runtime
CMD ["node", "dist/index.js"]
docker-compose.yml:
services:
app:
build: .
environment:
- NODE_ENV=production
- APP_PORT=3000
- APP_DATABASE__URL=postgresql://db:5432/myapp
env_file:
- .env.production
Kubernetes
Use ConfigMaps and Secrets:
ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_PORT: "3000"
APP_HOST: "0.0.0.0"
APP_NODE_ENV: "production"
APP_DATABASE__POOL__SIZE: "50"
Secret:
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
stringData:
SECRET_DATABASE__PASSWORD: "my-secure-password"
SECRET_API_KEY: "my-api-key"
Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets
Multi-Environment Setup
Development
// config/development.ts
import { FileConfigProvider } from '@stratix/config-file';
import { EnvConfigProvider } from '@stratix/config-env';
import { CompositeConfigProvider } from '@stratix/config-composite';
export const config = new CompositeConfigProvider({
providers: [
new EnvConfigProvider({ prefix: 'APP_', loadDotenv: true }),
new FileConfigProvider({
files: ['./config/development.json'],
watch: true, // Hot reload in dev
}),
],
strategy: 'first-wins',
});
Production
// config/production.ts
import { EnvConfigProvider } from '@stratix/config-env';
export const config = new EnvConfigProvider({
prefix: 'APP_',
loadDotenv: false, // Don't use .env files in production
schema: ProductionConfigSchema, // Strict validation
});
Configuration Factory
// config/index.ts
export function createConfig() {
const nodeEnv = process.env.NODE_ENV || 'development';
switch (nodeEnv) {
case 'production':
return import('./production').then(m => m.config);
case 'staging':
return import('./staging').then(m => m.config);
default:
return import('./development').then(m => m.config);
}
}
Monitoring
Log Configuration on Startup
async function bootstrap() {
const config = await configProvider.getAll();
console.log('Configuration loaded:');
console.log(` Environment: ${config.nodeEnv}`);
console.log(` Port: ${config.server.port}`);
console.log(` Database: ${sanitizeUrl(config.database.url)}`);
// Don't log secrets
}
function sanitizeUrl(url: string): string {
try {
const parsed = new URL(url);
parsed.password = '***';
return parsed.toString();
} catch {
return '***';
}
}
Health Checks
class ConfigHealthCheck implements HealthCheck {
constructor(private readonly config: ConfigProvider) {}
async check(): Promise<HealthCheckResult> {
try {
await this.config.has('server.port');
return {
status: 'healthy',
details: { config: 'loaded' },
};
} catch (error) {
return {
status: 'unhealthy',
details: { config: 'failed', error: String(error) },
};
}
}
}
Summary
Key best practices:
- Security: Separate secrets from config, never commit sensitive data
- Validation: Use schemas, validate at startup, fail fast
- Type Safety: Use TypeScript interfaces and type inference
- Defaults: Provide sensible defaults for optional values
- Documentation: Document required variables and provide examples
- Testing: Mock configuration in tests
- Performance: Cache frequently accessed values
- Containers: Use ENV variables, ConfigMaps, and Secrets
- Monitoring: Log configuration (sanitized) and implement health checks
Next Steps
- EnvConfigProvider - Environment variables
- FileConfigProvider - JSON/YAML files
- CompositeConfigProvider - Multiple sources
- Validation Guide - Schema validation