alitype` library addresses this by providing zero-dependency validation with automatic type inference and structured error reporting. Below is a production-grade implementation pattern.
Step 1: Isolate the Configuration Boundary
Create a dedicated module that acts as the single source of truth for runtime configuration. This module should be imported early in the application lifecycle, ensuring validation occurs before any business logic executes.
// src/config/runtime.ts
import { validateValue, validators, ValidationError } from 'valitype';
export class PlatformConfig {
public readonly gatewayUrl: string;
public readonly telemetryLevel: 'trace' | 'debug' | 'info' | 'warn' | 'error';
public readonly deploymentStage: 'sandbox' | 'staging' | 'production';
public readonly retryLimit: number;
public readonly signingKey: string;
constructor() {
this.gatewayUrl = validateValue('GATEWAY_ENDPOINT', process.env.GATEWAY_ENDPOINT, {
type: 'url',
required: true,
});
this.telemetryLevel = validateValue('LOG_VERBOSITY', process.env.LOG_VERBOSITY, {
type: { enum: ['trace', 'debug', 'info', 'warn', 'error'] },
default: 'info',
});
this.deploymentStage = validateValue('DEPLOY_ENV', process.env.DEPLOY_ENV, {
type: { enum: ['sandbox', 'staging', 'production'] },
required: true,
});
this.retryLimit = validateValue('MAX_RETRIES', process.env.MAX_RETRIES, {
type: 'number',
default: 3,
});
this.signingKey = validateValue('JWT_SECRET', process.env.JWT_SECRET, {
type: 'custom',
validator: validators.regex(/^[A-Za-z0-9_-]{32,64}$/),
required: true,
});
}
}
Step 2: Leverage Automatic Type Inference
valitype infers the return type directly from the validation rule. No manual type casting or as assertions are required. The gatewayUrl property is strictly string, retryLimit is strictly number, and telemetryLevel is narrowed to the exact union type defined in the enum rule. This eliminates type drift between configuration expectations and actual runtime values.
Step 3: Enforce Fail-Fast Initialization
Instantiate the configuration class at the entry point of your application. If any validation rule fails, the module throws a ValidationError immediately, preventing the application from starting with corrupted state.
// src/index.ts
import { PlatformConfig } from './config/runtime';
try {
const config = new PlatformConfig();
console.log(`Configuration loaded for ${config.deploymentStage}`);
// Initialize application services...
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Config validation failed: ${error.code} | Key: ${error.key} | Value: ${error.value}`);
process.exit(1);
}
throw error;
}
Architecture Decisions and Rationale
- Zero-Dependency Validation:
valitype avoids pulling in heavy schema libraries. This keeps bundle size minimal for frontend builds and reduces attack surface in backend services.
- Strict Parsing Over Permissive Coercion: The library rejects hexadecimal numbers (
0xff), scientific notation (1e5), and empty strings for numeric rules. It also enforces http or https schemes for URLs, preventing accidental acceptance of file:// or ftp:// protocols. This explicitness matches production configuration expectations.
- Structured Error Codes: Validation failures emit machine-readable codes (
REQUIRED, INVALID_NUMBER, INVALID_URL, INVALID_ENUM, INVALID_CUSTOM). This enables programmatic error handling in CI pipelines, CLI tools, and monitoring dashboards.
- Separation of Public and Private Config: Frontend frameworks like Vite (
VITE_ prefix) and Next.js (NEXT_PUBLIC_ prefix) expose environment variables at build time. The validation pattern should be duplicated across separate modules to maintain security boundaries. Server-only secrets must never pass through public validation layers.
Pitfall Guide
1. Silent Boolean Coercion
Explanation: JavaScript's Boolean() constructor treats any non-empty string as true, including 'false', '0', and 'no'. This causes feature flags and debug toggles to activate incorrectly.
Fix: Use strict boolean validation that only accepts 'true' or 'false' strings, or explicitly map allowed string values to boolean states.
2. Mixing Public and Private Configuration Boundaries
Explanation: Validating server secrets and browser-exposed variables in the same module risks accidentally bundling sensitive data into client-side code.
Fix: Maintain separate configuration modules for server and public contexts. Apply framework-specific prefix rules (VITE_, NEXT_PUBLIC_) and validate each boundary independently.
3. Overlooking Default Value Semantics
Explanation: Providing a default value does not bypass validation. If the environment variable is missing, the validator applies the default and continues. Developers sometimes assume defaults imply optional validation, leading to unexpected type mismatches.
Fix: Explicitly define required: false when defaults are intended, and verify that default values match the expected type and constraints.
4. Swallowing Validation Exceptions
Explanation: Catching ValidationError and logging it without exiting allows the application to continue with incomplete configuration. This creates inconsistent state and masks deployment failures.
Fix: Treat configuration validation as a hard gate. Exit the process or halt the build when validation fails. Use structured error codes to route diagnostics to monitoring systems.
Explanation: valitype is optimized for flat, string-based environment inputs. Attempting to validate nested JSON, form payloads, or complex API responses with it introduces unnecessary complexity.
Fix: Reserve valitype for environment variables and CLI arguments. Use full schema validation libraries like Zod or Valibot for nested objects, request bodies, and external data contracts.
6. Skipping Pre-Build Validation in Frontend Apps
Explanation: Frontend frameworks inject environment variables at compile time. If validation only runs in the browser, invalid configurations are baked into the bundle, requiring a full rebuild to fix.
Fix: Integrate validation into the build pipeline. Run a prebuild script or CI step that imports the configuration module and verifies all variables before bundling.
7. Assuming CI/CD Injection Guarantees Correctness
Explanation: CI/CD platforms and container orchestrators inject variables as raw strings. They do not validate format, range, or schema. A typo in a pipeline definition or a missing secret manager rotation can inject malformed values.
Fix: Treat CI/CD injection as an untrusted input source. Validate all variables at application startup, regardless of deployment platform guarantees.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Flat environment variables with strict typing | valitype | Zero dependencies, automatic type inference, structured errors, fail-fast initialization | Low (minimal bundle/runtime overhead) |
| Nested configuration objects or JSON payloads | Zod / Valibot | Full schema validation, transformation pipelines, complex object parsing | Medium (larger dependency footprint) |
Simple .env loading without validation | dotenv | Lightweight environment injection, no type safety or validation | Low (high runtime risk) |
| CLI argument parsing with environment fallbacks | valitype + commander / yargs | Consistent validation across env vars and CLI flags, unified error handling | Low (moderate setup complexity) |
Configuration Template
// src/config/app.ts
import { validateValue, validators, ValidationError } from 'valitype';
export interface AppConfig {
readonly databaseUrl: string;
readonly cacheTtl: number;
readonly featureFlags: 'alpha' | 'beta' | 'stable';
readonly encryptionKey: string;
}
export function loadAppConfig(): AppConfig {
return {
databaseUrl: validateValue('DATABASE_URL', process.env.DATABASE_URL, {
type: 'url',
required: true,
}),
cacheTtl: validateValue('CACHE_TTL_SECONDS', process.env.CACHE_TTL_SECONDS, {
type: 'number',
default: 300,
}),
featureFlags: validateValue('RELEASE_CHANNEL', process.env.RELEASE_CHANNEL, {
type: { enum: ['alpha', 'beta', 'stable'] },
default: 'stable',
}),
encryptionKey: validateValue('ENCRYPTION_SECRET', process.env.ENCRYPTION_SECRET, {
type: 'custom',
validator: validators.regex(/^[A-Fa-f0-9]{64}$/),
required: true,
}),
};
}
// Entry point guard
try {
export const config = loadAppConfig();
} catch (err) {
if (err instanceof ValidationError) {
console.error(`[CONFIG] ${err.code} | ${err.key} | ${err.value}`);
process.exit(1);
}
throw err;
}
Quick Start Guide
- Install the package: Run
npm install valitype in your project root.
- Create a config module: Add
src/config/runtime.ts and import validateValue and validators.
- Define validation rules: Map each environment variable to a strict type (
url, number, boolean, enum, or custom). Set required: true for mandatory values and provide default values where appropriate.
- Initialize at startup: Import and instantiate the configuration module in your application entry point. Wrap initialization in a
try/catch block to handle ValidationError and exit gracefully.
- Verify in CI: Add a prebuild or startup script that imports the configuration module. If validation fails, the pipeline halts before deployment, preventing corrupted bundles or runtime crashes.