Configuration Guide

September 4, 2025 ยท View on GitHub

This guide explains how to use and extend the type-safe environment configuration system provided by the starter template.

Important: Configuration is mandatory. The application validates all environment variables at startup and will exit with clear error messages if configuration is invalid or missing.

Overview

The configuration module (src/config.ts) provides:

  • ๐Ÿ”’ Type-safe environment variable access with full TypeScript support
  • โœ… Validation at application startup using Zod schemas
  • ๐Ÿšจ Clear error messages when configuration is invalid
  • ๐Ÿ” Sensitive value masking in error outputs
  • ๐ŸŽฏ Sensible defaults for optional configuration
  • ๐Ÿ“ Auto-generated types from your schema

Quick Start

  1. Copy the example configuration:

    cp .env.example .env
    
  2. Edit .env with your values:

    NODE_ENV=development
    APP_NAME=my-app
    LOG_LEVEL=debug
    
  3. Use configuration in your code:

    import { config } from './config.js';
    
    // TypeScript knows config.ENABLE_METRICS is a boolean
    if (config.ENABLE_METRICS) {
      setupMetrics();
    }
    
    // Check if optional config exists
    if (config.DEBUG) {
      enableDebugMode(config.DEBUG);
    }
    

Complete Environment Variable Reference

This table lists all available environment variables, their types, requirements, and defaults.

Core Configuration

VariableTypeRequiredDefaultDescription
NODE_ENVenumOptionaldevelopmentEnvironment: development, production, test, staging
APP_NAMEstringOptionalagentic-node-ts-starterApplication name for logging and metrics

Logging Configuration

VariableTypeRequiredDefaultDescription
LOG_LEVELenumOptionalinfo/debug*Log level: trace, debug, info, warn, error, fatal, silent
LOG_OUTPUTenumOptionalstdoutOutput destination: stdout, stderr, file, syslog, null
DEBUGstringOptional-Debug namespaces to enable (e.g., app:*, express:*)
FORCE_COLORbooleanOptional-Force colored output in terminals (true/false, 1/0, yes/no)

* LOG_LEVEL defaults to info in production, debug in development

File Logging (when LOG_OUTPUT=file)

VariableTypeRequiredDefaultDescription
LOG_FILE_PATHstringOptional./logs/app.logPath to log file
LOG_FILE_MAX_SIZEstringOptional10MMax file size before rotation (e.g., 10M, 100M, 1G)
LOG_FILE_MAX_FILESnumberOptional5Number of rotated files to keep
LOG_FILE_PERMISSIONSstringOptional640Octal file permissions (e.g., 600, 644, 640)

Syslog Configuration (when LOG_OUTPUT=syslog)

VariableTypeRequiredDefaultDescription
LOG_SYSLOG_HOSTstringOptionallocalhostSyslog server hostname or IP
LOG_SYSLOG_PORTnumberOptional514Syslog server port
LOG_SYSLOG_PROTOCOLenumOptionaludpProtocol: udp or tcp

Feature Flags

VariableTypeRequiredDefaultDescription
ENABLE_METRICSbooleanOptionalfalseEnable metrics collection

Timeouts

VariableTypeRequiredDefaultDescription
TIMEOUT_MSnumberOptional30000General operation timeout in milliseconds

Test Configuration

VariableTypeRequiredDefaultDescription
LOG_TEST_FILE_TIMEOUTnumberOptional300Timeout for file write tests in CI (ms)

Adding Custom Configuration

To add your own environment variables:

  1. Update the schema in src/config.ts:

    const ConfigSchema = z.object({
      // ... existing configuration ...
    
      // Add your custom variables
      MY_CUSTOM_VAR: z.string().min(1).describe('My custom variable'),
    
      BATCH_SIZE: z
        .string()
        .regex(/^\d+$/)
        .transform(Number)
        .default('100')
        .describe('Processing batch size'),
    
      FEATURE_FLAG: BooleanSchema.default('false').describe('Enable new feature'),
    });
    
  2. TypeScript automatically provides types:

    import { config } from './config.js';
    
    // TypeScript knows about your new variables
    const batchSize = config.BATCH_SIZE;
    
    if (config.FEATURE_FLAG) {
      enableNewFeature();
    }
    
  3. Update .env.example:

    # Custom Configuration
    MY_CUSTOM_VAR=example-value
    
    # Processing Settings
    BATCH_SIZE=100
    
    # Feature Flags
    FEATURE_FLAG=false
    

Validation Types

The configuration module supports various validation types:

Strings

// Basic string
MY_VAR: z.string(),

// String with minimum length
SECRET: z.string().min(32),

// String matching pattern
LOCALE: z.string().regex(/^[a-z]{2}-[A-Z]{2}$/),

Numbers

// Number from string input
BATCH_SIZE: z.string().regex(/^\d+$/).transform(Number),

// Number with range validation
WORKERS: z.string()
  .transform(Number)
  .refine(n => n >= 1 && n <= 10),

Booleans

// Boolean accepting multiple formats
ENABLE_FEATURE: BooleanSchema, // accepts: true/false, 1/0, yes/no

Enums

// String enum
ENVIRONMENT: z.enum(['development', 'staging', 'production']),

// With default
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),

Arrays

// Comma-separated list
ALLOWED_HOSTS: z.string()
  .transform(s => s.split(',').map(h => h.trim()))
  .default('localhost'),

Error Handling

Validation Failure Examples

When configuration validation fails at startup, you'll see clear, actionable error messages:

Example 1: Missing Required Variables

โŒ Invalid environment configuration:

Missing required variables:
  โ€ข API_KEY: Required but not provided
  โ€ข DATABASE_URL: Required but not provided

๐Ÿ’ก Tip: Check .env.example for valid configuration examples

Example 2: Invalid Formats and Types

โŒ Invalid environment configuration:

Invalid format:
  โ€ข NODE_ENV: Invalid enum value (received: "dev")
  โ€ข TIMEOUT_MS: Expected number, received string (received: "thirty-seconds")
  โ€ข LOG_LEVEL: Invalid enum value (received: "verbose")
  โ€ข ENABLE_METRICS: Invalid enum value (received: "maybe")

Other errors:
  โ€ข LOG_SYSLOG_PORT: Number must be less than or equal to 65535 (received: "99999")
  โ€ข LOG_FILE_PERMISSIONS: Invalid octal format (received: "777xyz")

๐Ÿ’ก Tip: Check .env.example for valid configuration examples

Sensitive Value Protection

The configuration system automatically masks sensitive values in error messages to prevent accidental exposure in logs or console output.

How It Works

  • Detection: Any variable name containing PASSWORD, TOKEN, SECRET, or KEY is considered sensitive
  • Masking: Shows only first 2 and last 2 characters for values longer than 4 characters
  • Short values: Completely masked as ***

Example: Sensitive Values in Errors

โŒ Invalid environment configuration:

Invalid format:
  โ€ข API_TOKEN: String must contain at least 32 character(s) (received: "sk***yz")
  โ€ข DATABASE_PASSWORD: Invalid format (received: "pa***rd")
  โ€ข SECRET_KEY: Must match pattern (received: "se***89")
  โ€ข AUTH_SECRET: Required but not provided

Other errors:
  โ€ข ENCRYPTION_KEY: String must be exactly 64 characters (received: "ke***01")

๐Ÿ’ก Tip: Check .env.example for valid configuration examples

Note: Even though values are masked in error messages, the actual validation still occurs with the full values. This ensures security without compromising functionality.

Helper Functions

The configuration module provides helper functions for common tasks:

getConfig(key)

Type-safe configuration getter:

import { getConfig } from './config.js';

const logLevel = getConfig('LOG_LEVEL'); // TypeScript knows this is a string
const metrics = getConfig('ENABLE_METRICS'); // TypeScript knows this is a boolean

hasConfig(key)

Check if optional configuration exists:

import { hasConfig, config } from './config.js';

if (hasConfig('DEBUG')) {
  // TypeScript knows config.DEBUG is defined here
  enableDebugMode(config.DEBUG);
}

getConfigKeys()

Get all configuration keys (useful for debugging):

import { getConfigKeys } from './config.js';

console.log('Loaded configuration keys:', getConfigKeys());

Best Practices

1. Fail Fast

Configuration is validated at application startup. Invalid configuration causes immediate exit with clear error messages.

2. Use TypeScript

Let TypeScript guide you:

// โœ… TypeScript knows the types
if (config.TIMEOUT_MS > 5000) {
  // TIMEOUT_MS is number
  // ...
}

// โŒ TypeScript error: Property 'UNKNOWN' does not exist
console.log(config.UNKNOWN);

3. Document Your Variables

Always add descriptions to your schema:

MY_VARIABLE: z.string()
  .describe('Controls the widget behavior in production'),

4. Provide Defaults When Sensible

// Good: Sensible default for optional feature
CACHE_TTL: z.string()
  .transform(Number)
  .default('3600'), // 1 hour default

// Good: No default for required custom config
API_ENDPOINT: z.string().url(), // No default, must be provided

Organize your schema logically:

const ConfigSchema = z.object({
  // Core
  NODE_ENV: /* ... */,
  APP_NAME: /* ... */,

  // Logging
  LOG_LEVEL: /* ... */,
  DEBUG: /* ... */,

  // Feature Flags
  ENABLE_FEATURE_X: /* ... */,
  ENABLE_FEATURE_Y: /* ... */,
});

Deployment Considerations

Docker

Set environment variables in your Dockerfile:

ENV NODE_ENV=production
ENV LOG_LEVEL=info

Or use docker-compose:

services:
  app:
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=info
      - ENABLE_METRICS=true

Kubernetes

Use ConfigMaps and Secrets:

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  NODE_ENV: production
  LOG_LEVEL: info
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
stringData:
  MY_SECRET: your-secret-value

Cloud Platforms

  • Heroku: Set via heroku config:set
  • Vercel: Configure in project settings
  • AWS: Use Parameter Store or Secrets Manager
  • Azure: Use App Configuration or Key Vault

CI/CD

Never commit .env files. Instead:

  1. Use .env.example as documentation
  2. Set secrets in CI/CD environment
  3. Use platform-specific secret management

Example GitHub Actions:

- name: Run tests
  env:
    NODE_ENV: test
    LOG_LEVEL: silent
  run: pnpm test

Testing

Unit Tests

Test configuration with different environment setups:

describe('Config', () => {
  const originalEnv = process.env;

  beforeEach(() => {
    vi.resetModules();
    process.env = { ...originalEnv };
  });

  it('should load production config', async () => {
    process.env.NODE_ENV = 'production';
    process.env.API_KEY = 'test-api-key';

    const { config } = await import('./config.js');

    expect(config.NODE_ENV).toBe('production');
    expect(config.LOG_LEVEL).toBe('info'); // production default
  });
});

Integration Tests

Use test-specific configuration:

// test.env
NODE_ENV = test;
LOG_LEVEL = silent;
TIMEOUT_MS = 1000;

Troubleshooting

Common Issues

  1. "Invalid environment configuration" on startup

    • Check error message for specific variables
    • Verify .env file is in project root
    • Ensure values match expected format
  2. TypeScript doesn't recognize new variables

    • Restart TypeScript server in your IDE
    • Ensure you've added to ConfigSchema
    • Check for typos in variable names
  3. Configuration works locally but not in production

    • Verify all required variables are set in production
    • Check for different Node.js versions
    • Ensure .env is not being used in production
  4. Sensitive values appearing in logs

    • Add variable names to SENSITIVE_KEYS array
    • Use structured logging with redaction
    • Review error handling code

Debug Mode

Enable debug output for configuration loading:

// In src/config.ts, temporarily add:
console.log('Raw environment:', process.env);
console.log('Parsed config:', config);

Migration Guide

From Plain process.env

Before:

const port = process.env.PORT || 3000;
const enableMetrics = process.env.ENABLE_METRICS === 'true';

After:

import { config } from './config.js';

const port = config.PORT; // Already parsed as number
const enableMetrics = config.ENABLE_METRICS; // Already boolean

From dotenv

Before:

import dotenv from 'dotenv';
dotenv.config();

const apiKey = process.env.API_KEY;
if (!apiKey) {
  throw new Error('API_KEY is required');
}

After:

import { config } from './config.js';

// Validation happens automatically
const apiKey = config.API_KEY; // TypeScript knows this exists if required

Summary

The configuration module provides a robust, type-safe way to manage environment variables in your application. By validating configuration at startup and providing clear error messages, it helps catch configuration issues early in development rather than in production.

Key benefits:

  • โœ… Type safety: Full TypeScript support
  • โœ… Validation: Catch errors at startup
  • โœ… Documentation: Self-documenting with descriptions
  • โœ… Security: Automatic sensitive value masking
  • โœ… Developer experience: Clear errors and sensible defaults

For more examples and patterns, see the test file at tests/config.spec.ts.