Skip to content

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:

typescript
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

FieldTypeRulesDescription
namestringmin(1)Application name, cannot be empty
versionstringsemver regexMust follow semantic versioning (x.y.z)
environmentenumdevelopment/production/testRuntime environment

Example:

json
{
  "app": {
    "name": "my-application",
    "version": "1.2.3",
    "environment": "production"
  }
}

Server Section

FieldTypeRulesDescription
portnumber1-65535Valid port number
hoststringmin(1)Server hostname or IP address
timeoutnumber1000-300000msRequest timeout in milliseconds

Example:

json
{
  "server": {
    "port": 3000,
    "host": "0.0.0.0",
    "timeout": 30000
  }
}

Logging Section

FieldTypeRulesDescription
levelenumerror/warn/info/debugLog level
outputFilestring|null-Optional log file path
includeTimestampboolean-Whether to include timestamps
useColorsboolean-Whether to use colored output

Example:

json
{
  "logging": {
    "level": "info",
    "outputFile": "logs/app.log",
    "includeTimestamp": true,
    "useColors": false
  }
}

Features Section

FieldTypeRulesDescription
enableMetricsboolean-Enable metrics collection
enableHealthCheckboolean-Enable health check endpoint
enableCorsboolean-Enable CORS middleware

Example:

json
{
  "features": {
    "enableMetrics": true,
    "enableHealthCheck": true,
    "enableCors": false
  }
}

Validation Process

Automatic Validation

The ConfigManager automatically validates configuration when loading:

typescript
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:

typescript
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:

typescript
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

json
{
  "app": {
    "name": "",  // ❌ Empty string
    "version": "1.0.0",
    "environment": "development"
  }
}

Error: app.name: String must contain at least 1 character(s)

Invalid Version Format

json
{
  "app": {
    "name": "my-app",
    "version": "1.0",  // ❌ Not semver format
    "environment": "development"
  }
}

Error: app.version: Version must follow semver format

Invalid Environment

json
{
  "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

json
{
  "server": {
    "port": 70000,  // ❌ Above maximum
    "host": "localhost",
    "timeout": 30000
  }
}

Error: server.port: Number must be less than or equal to 65535

Invalid Timeout Range

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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:

typescript
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:

typescript
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

bash
#!/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

yaml
# .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

typescript
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

typescript
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.');
}

Next Steps

Released under the MIT License.