Configuration Validation
Learn how configuration validation works in the AI-Enhanced TypeScript Template using Zod schemas.
Overview
Configuration validation ensures that your application configuration files are correct before your application starts. This prevents runtime errors and provides clear feedback about configuration issues.
Validation Schema
The configuration is validated using the AppConfigSchema
Zod schema:
import { z } from 'zod';
export const AppConfigSchema = z.object({
app: z.object({
name: z.string().min(1, 'App name cannot be empty'),
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must follow semver format'),
environment: z.enum(['development', 'production', 'test'])
}),
server: z.object({
port: z.number().int().min(1).max(65535),
host: z.string().min(1, 'Host cannot be empty'),
timeout: z.number().int().min(1000).max(300000)
}),
logging: z.object({
level: z.enum(['error', 'warn', 'info', 'debug']),
outputFile: z.string().nullable(),
includeTimestamp: z.boolean(),
useColors: z.boolean()
}),
features: z.object({
enableMetrics: z.boolean(),
enableHealthCheck: z.boolean(),
enableCors: z.boolean()
})
});
export type AppConfig = z.infer<typeof AppConfigSchema>;
Validation Rules
App Section
Field | Type | Rules | Description |
---|---|---|---|
name | string | min(1) | Application name, cannot be empty |
version | string | semver regex | Must follow semantic versioning (x.y.z) |
environment | enum | development/production/test | Runtime environment |
Example:
{
"app": {
"name": "my-application",
"version": "1.2.3",
"environment": "production"
}
}
Server Section
Field | Type | Rules | Description |
---|---|---|---|
port | number | 1-65535 | Valid port number |
host | string | min(1) | Server hostname or IP address |
timeout | number | 1000-300000ms | Request timeout in milliseconds |
Example:
{
"server": {
"port": 3000,
"host": "0.0.0.0",
"timeout": 30000
}
}
Logging Section
Field | Type | Rules | Description |
---|---|---|---|
level | enum | error/warn/info/debug | Log level |
outputFile | string|null | - | Optional log file path |
includeTimestamp | boolean | - | Whether to include timestamps |
useColors | boolean | - | Whether to use colored output |
Example:
{
"logging": {
"level": "info",
"outputFile": "logs/app.log",
"includeTimestamp": true,
"useColors": false
}
}
Features Section
Field | Type | Rules | Description |
---|---|---|---|
enableMetrics | boolean | - | Enable metrics collection |
enableHealthCheck | boolean | - | Enable health check endpoint |
enableCors | boolean | - | Enable CORS middleware |
Example:
{
"features": {
"enableMetrics": true,
"enableHealthCheck": true,
"enableCors": false
}
}
Validation Process
Automatic Validation
The ConfigManager
automatically validates configuration when loading:
import { ConfigManager } from './config.js';
const configManager = new ConfigManager();
const result = configManager.loadConfig();
if (result.success) {
// Configuration is valid and typed
const config = result.data; // Type: AppConfig
console.log(`Starting ${config.app.name} v${config.app.version}`);
} else {
// Validation failed
console.error('Configuration validation failed:', result.error);
process.exit(1);
}
Manual Validation
You can also validate configuration manually:
import { validateAppConfig } from './validation.js';
// Load raw configuration data
const rawConfig = JSON.parse(fs.readFileSync('config.json', 'utf-8'));
// Validate manually
const result = validateAppConfig(rawConfig);
if (result.success) {
console.log('Configuration is valid');
return result.data;
} else {
console.error('Validation failed:', result.error);
throw new Error(`Invalid configuration: ${result.error}`);
}
Static File Validation
Validate configuration files without loading them into a ConfigManager:
import { ConfigManager } from './config.js';
// Validate a specific config file
const result = ConfigManager.validateConfigFile('./config/production.json');
if (result.success) {
console.log('Production config is valid');
} else {
console.error('Production config is invalid:', result.error);
}
Common Validation Errors
Invalid App Name
{
"app": {
"name": "", // ❌ Empty string
"version": "1.0.0",
"environment": "development"
}
}
Error: app.name: String must contain at least 1 character(s)
Invalid Version Format
{
"app": {
"name": "my-app",
"version": "1.0", // ❌ Not semver format
"environment": "development"
}
}
Error: app.version: Version must follow semver format
Invalid Environment
{
"app": {
"name": "my-app",
"version": "1.0.0",
"environment": "staging" // ❌ Not in allowed enum
}
}
Error: app.environment: Invalid enum value. Expected 'development' | 'production' | 'test', received 'staging'
Invalid Port Range
{
"server": {
"port": 70000, // ❌ Above maximum
"host": "localhost",
"timeout": 30000
}
}
Error: server.port: Number must be less than or equal to 65535
Invalid Timeout Range
{
"server": {
"port": 3000,
"host": "localhost",
"timeout": 500 // ❌ Below minimum
}
}
Error: server.timeout: Number must be greater than or equal to 1000
Environment-Specific Validation
Development Configuration
{
"app": {
"name": "my-app-dev",
"version": "1.0.0-dev",
"environment": "development"
},
"server": {
"port": 3000,
"host": "localhost",
"timeout": 30000
},
"logging": {
"level": "debug",
"outputFile": null,
"includeTimestamp": true,
"useColors": true
},
"features": {
"enableMetrics": false,
"enableHealthCheck": true,
"enableCors": true
}
}
Production Configuration
{
"app": {
"name": "my-app",
"version": "1.0.0",
"environment": "production"
},
"server": {
"port": 8080,
"host": "0.0.0.0",
"timeout": 60000
},
"logging": {
"level": "info",
"outputFile": "/var/log/app.log",
"includeTimestamp": true,
"useColors": false
},
"features": {
"enableMetrics": true,
"enableHealthCheck": true,
"enableCors": false
}
}
Test Configuration
{
"app": {
"name": "my-app-test",
"version": "1.0.0",
"environment": "test"
},
"server": {
"port": 0,
"host": "localhost",
"timeout": 5000
},
"logging": {
"level": "error",
"outputFile": null,
"includeTimestamp": false,
"useColors": false
},
"features": {
"enableMetrics": false,
"enableHealthCheck": false,
"enableCors": false
}
}
Custom Validation
Extending the Schema
You can extend the base configuration schema for your specific needs:
import { AppConfigSchema } from './validation.js';
// Extend with custom fields
const ExtendedConfigSchema = AppConfigSchema.extend({
database: z.object({
host: z.string(),
port: z.number().int().min(1).max(65535),
name: z.string().min(1),
ssl: z.boolean().default(false)
}),
redis: z.object({
host: z.string(),
port: z.number().int().min(1).max(65535),
password: z.string().optional()
}).optional()
});
export type ExtendedConfig = z.infer<typeof ExtendedConfigSchema>;
// Create custom validation function
export function validateExtendedConfig(data: unknown) {
try {
const config = ExtendedConfigSchema.parse(data);
return { success: true, data: config };
} catch (error) {
if (error instanceof z.ZodError) {
const validationError = new ValidationError('Extended config validation failed', error.issues);
return { success: false, error: validationError.getFormattedMessage() };
}
return { success: false, error: 'Unknown validation error' };
}
}
Cross-Field Validation
Add validation rules that depend on multiple fields:
const ConfigWithCrossValidation = AppConfigSchema.refine(
(config) => {
// In production, logging should go to a file
if (config.app.environment === 'production' && !config.logging.outputFile) {
return false;
}
return true;
},
{
message: 'Production environment requires logging to a file',
path: ['logging', 'outputFile']
}
).refine(
(config) => {
// High timeout should not be used in test environment
if (config.app.environment === 'test' && config.server.timeout > 10000) {
return false;
}
return true;
},
{
message: 'Test environment should use shorter timeouts',
path: ['server', 'timeout']
}
);
Validation in CI/CD
Pre-deployment Validation
#!/bin/bash
# validate-config.sh
echo "Validating configuration files..."
# Validate all environment configs
for env in development staging production; do
echo "Validating config.$env.json..."
node -e "
const { ConfigManager } = require('./dist/config.js');
const result = ConfigManager.validateConfigFile('./config/config.$env.json');
if (!result.success) {
console.error('❌ $env config invalid:', result.error);
process.exit(1);
}
console.log('✅ $env config valid');
"
done
echo "All configuration files are valid!"
GitHub Actions Validation
# .github/workflows/validate-config.yml
name: Validate Configuration
on:
push:
paths:
- 'config/**'
- 'src/validation.ts'
- 'src/config.ts'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run build
- name: Validate configurations
run: |
for config in config/*.json; do
echo "Validating $config..."
node -e "
const { ConfigManager } = require('./dist/config.js');
const result = ConfigManager.validateConfigFile('$config');
if (!result.success) {
console.error('Invalid config:', result.error);
process.exit(1);
}
console.log('✅ Valid');
"
done
Error Handling Best Practices
Graceful Degradation
function loadConfigWithFallback() {
const configManager = new ConfigManager();
const result = configManager.loadConfig();
if (result.success) {
return configManager;
}
// Log the error but continue with defaults
console.warn('Configuration validation failed, using defaults:', result.error);
// Create a minimal valid configuration
const defaultConfig = {
app: { name: 'app', version: '1.0.0', environment: 'development' },
server: { port: 3000, host: 'localhost', timeout: 30000 },
logging: { level: 'info', outputFile: null, includeTimestamp: true, useColors: true },
features: { enableMetrics: false, enableHealthCheck: true, enableCors: false }
};
// Validate the default config (should always pass)
const defaultResult = validateAppConfig(defaultConfig);
if (!defaultResult.success) {
throw new Error('Default configuration is invalid - this should never happen');
}
// Create a new config manager with the default config
// Note: This would require extending ConfigManager to accept config data directly
return createConfigManagerFromData(defaultResult.data);
}
Detailed Error Reporting
function reportConfigurationError(error: string) {
console.error('Configuration Validation Failed');
console.error('================================');
console.error(error);
console.error('');
console.error('Please check your configuration file and ensure:');
console.error('- All required fields are present');
console.error('- Field types match the expected types');
console.error('- Enum values are from the allowed list');
console.error('- Number values are within the specified ranges');
console.error('');
console.error('See the documentation for the complete configuration schema.');
}