npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

type-safe-config-loader

v1.0.0

Published

Type-safe configuration management for Node.js/TypeScript with environment variable and config file support

Readme

Type-Safe Config Loader

npm version TypeScript License: MIT npm downloads

A modern, developer-friendly configuration management library for Node.js and TypeScript. Get type-safe, validated configuration from environment variables and files with zero runtime surprises.

Why Type-Safe Config Loader?

Stop debugging configuration issues in production. Traditional config management in Node.js is error-prone:

  • process.env.PORT is always a string (or undefined)
  • ❌ Missing required config only discovered at runtime
  • ❌ Type mismatches cause silent failures
  • ❌ Secrets accidentally logged in error messages
  • ❌ No single source of truth for required configuration

Type-Safe Config Loader solves these problems:

  • Type-Safe: Full TypeScript support with auto-completion
  • Fail-Fast: Validate configuration at startup, not runtime
  • Multi-Source: Environment variables, .env files, YAML, JSON
  • Secure: Automatic masking of sensitive values
  • Zero Config: Works out of the box, configurable when needed
  • Developer-Friendly: Clear error messages and excellent DX
// Before: Unsafe, untyped, error-prone
const port = parseInt(process.env.PORT || '3000'); // 😱
const dbUrl = process.env.DATABASE_URL; // string | undefined 😱

// After: Type-safe, validated, bulletproof
const config = loadConfig(schema);
config.port; // number ✅
config.database.url; // string ✅

Quick Start

1. Install

npm install type-safe-config-loader zod

2. Define Your Schema

// config.ts
import { z } from 'zod';
import { loadConfig, defineConfig } from 'type-safe-config-loader';

const configSchema = defineConfig({
  PORT: z.coerce.number().int().positive().default(3000),
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
  DEBUG: z.coerce.boolean().default(false),
});

export const config = loadConfig(configSchema, {
  dotenv: true,
  sensitive: ['API_KEY', 'DATABASE_URL']
});

3. Use Everywhere

// server.ts
import { config } from './config';

app.listen(config.PORT, () => {
  console.log(`Server running on port ${config.PORT}`);
  // config.PORT is typed as number ✅
  // config.API_KEY is typed as string ✅
  // config.MISSING_PROP // TypeScript error ✅
});

4. Environment Setup

# .env (development)
DATABASE_URL=postgres://localhost:5432/myapp
API_KEY=dev-key-123
DEBUG=true

That's it! Your configuration is now type-safe, validated, and secure.

Installation

# npm
npm install type-safe-config-loader zod

# yarn
yarn add type-safe-config-loader zod

# pnpm
pnpm add type-safe-config-loader zod

Requirements:

  • Node.js 16+
  • TypeScript 4.5+ (for optimal type inference)

Usage Examples

Basic Configuration

import { z } from 'zod';
import { loadConfig, defineConfig } from 'type-safe-config-loader';

// Define what your app needs
const schema = defineConfig({
  PORT: z.coerce.number().default(3000),
  HOST: z.string().default('localhost'),
  LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});

// Load and validate
const config = loadConfig(schema);

// Use with full type safety
console.log(`Server: http://${config.HOST}:${config.PORT}`);

Advanced Schema with Nested Objects

const advancedSchema = defineConfig({
  // Server configuration
  server: z.object({
    port: z.coerce.number().int().positive().default(3000),
    host: z.string().default('localhost'),
    cors: z.object({
      enabled: z.coerce.boolean().default(true),
      origins: z.string().transform(s => s.split(',')).default('*'),
    }),
  }),
  
  // Database configuration
  database: z.object({
    url: z.string().url(),
    pool: z.object({
      min: z.coerce.number().int().min(0).default(2),
      max: z.coerce.number().int().min(1).default(10),
    }),
    ssl: z.coerce.boolean().default(false),
  }),
  
  // Feature flags
  features: z.object({
    analytics: z.coerce.boolean().default(false),
    cache: z.object({
      enabled: z.coerce.boolean().default(true),
      ttl: z.coerce.number().int().positive().default(3600),
    }),
  }).default({}),
  
  // External services
  services: z.object({
    redis: z.object({
      url: z.string().url().optional(),
      keyPrefix: z.string().default('app:'),
    }).optional(),
  }).default({}),
});

const config = loadConfig(advancedSchema, {
  file: 'config.yaml',
  sensitive: ['database.url', 'services.redis.url']
});

// Fully typed access
config.server.port; // number
config.database.pool.max; // number
config.features.cache.enabled; // boolean

Environment-Specific Configuration

// Supports dynamic file loading based on NODE_ENV
const config = loadConfig(schema, {
  file: 'config.${NODE_ENV}.yaml', // config.development.yaml, config.production.yaml
  dotenv: true,
  strict: true
});

File Structure:

config/
├── config.development.yaml
├── config.production.yaml
└── config.test.yaml

Multiple Configuration Sources

const config = loadConfig(schema, {
  file: ['config/base.yaml', 'config/overrides.yaml'],
  dotenv: '.env.local',
  envPrefix: 'MYAPP_' // Only load env vars starting with MYAPP_
});

Loading Priority (highest to lowest):

  1. Environment variables
  2. Config files (later files override earlier ones)
  3. Schema defaults

Async Configuration Loading

import { loadConfigAsync } from 'type-safe-config-loader';

async function initializeApp() {
  const config = await loadConfigAsync(schema, {
    file: 'config.yaml'
  });
  
  // Start your app with validated config
  startServer(config);
}

Custom Validation and Transforms

const schema = defineConfig({
  // Custom validation
  email: z.string().email(),
  
  // Transform values
  tags: z.string().transform(s => s.split(',').map(t => t.trim())),
  
  // Complex validation
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain uppercase letter')
    .regex(/[0-9]/, 'Password must contain number'),
  
  // Conditional validation
  httpsPort: z.coerce.number().int().positive().optional()
    .refine((port, ctx) => {
      if (ctx.parent.NODE_ENV === 'production' && !port) {
        throw new Error('HTTPS port required in production');
      }
      return true;
    }),
});

API Reference

loadConfig(schema, options?)

Synchronously loads and validates configuration.

Parameters:

  • schema: ZodSchema | ConfigSchemaDefinition - Configuration schema
  • options?: ConfigOptions - Loading options

Returns: Validated configuration object with full TypeScript types

Example:

const config = loadConfig(mySchema, {
  file: 'config.yaml',
  dotenv: true
});

loadConfigAsync(schema, options?)

Asynchronously loads and validates configuration.

Parameters:

  • schema: ZodSchema | ConfigSchemaDefinition - Configuration schema
  • options?: ConfigOptions - Loading options

Returns: Promise<ValidatedConfig>

Example:

const config = await loadConfigAsync(mySchema, {
  file: 'config.yaml'
});

defineConfig<T>(shape: T)

Helper function to create a Zod object schema with better TypeScript inference.

Parameters:

  • shape: ZodRawShape - Schema shape definition

Returns: ZodObject<T>

Example:

const schema = defineConfig({
  PORT: z.coerce.number().default(3000),
  DEBUG: z.coerce.boolean().default(false)
});

printConfigSchema(schema)

Generates human-readable documentation of your configuration schema.

Parameters:

  • schema: ZodSchema | ConfigSchemaDefinition - Configuration schema

Returns: string - Formatted schema documentation

Example:

console.log(printConfigSchema(mySchema));
// Output:
// Expected Configuration:
// - PORT (number) [Optional] Default: 3000
// - DATABASE_URL (string) [Required]
// - DEBUG (boolean) [Optional] Default: false

Configuration Options

ConfigOptions

interface ConfigOptions {
  /** Config file path(s) to load */
  file?: string | string[];
  
  /** Load .env file (true, false, or custom path) */
  dotenv?: boolean | string;
  
  /** Only load env vars with this prefix */
  envPrefix?: string;
  
  /** Require all files to exist */
  strict?: boolean;
  
  /** Exit process on validation error */
  exitOnError?: boolean;
  
  /** Custom error handler */
  onError?: (errors: ConfigError[]) => void;
  
  /** Keys to mask in logs/errors */
  sensitive?: string[];
}

Default Behavior

const defaultOptions: ConfigOptions = {
  dotenv: false,
  strict: false,
  exitOnError: true,
  sensitive: []
};

File Format Support

YAML Configuration

# config.yaml
server:
  port: 3000
  host: localhost

database:
  url: postgres://localhost:5432/myapp
  pool:
    min: 2
    max: 10

features:
  analytics: false
  cache:
    enabled: true
    ttl: 3600

JSON Configuration

{
  "server": {
    "port": 3000,
    "host": "localhost"
  },
  "database": {
    "url": "postgres://localhost:5432/myapp",
    "pool": {
      "min": 2,
      "max": 10
    }
  },
  "features": {
    "analytics": false,
    "cache": {
      "enabled": true,
      "ttl": 3600
    }
  }
}

Environment Variables

# .env
SERVER_PORT=3000
SERVER_HOST=localhost
DATABASE_URL=postgres://localhost:5432/myapp
DATABASE_POOL_MIN=2
DATABASE_POOL_MAX=10
FEATURES_ANALYTICS=false
FEATURES_CACHE_ENABLED=true
FEATURES_CACHE_TTL=3600

Environment Variable Mapping:

  • Nested objects: PARENT_CHILD_FIELD
  • Arrays: TAGS=tag1,tag2,tag3 (with transform)
  • Booleans: true, false, 1, 0
  • Numbers: Auto-parsed with z.coerce.number()

Error Handling

Validation Errors

When configuration is invalid, you get clear, actionable error messages:

// Missing required field
Config validation failed with 1 error:
- API_KEY: Required field is missing

// Type mismatch  
Config validation failed with 1 error:
- PORT: Expected number, received "not-a-number"

// Multiple errors
Config validation failed with 3 errors:
- PORT: Expected number, received "abc"
- DATABASE_URL: Invalid URL format
- LOG_LEVEL: Invalid enum value. Expected 'debug' | 'info' | 'warn' | 'error', received 'verbose'

Custom Error Handling

const config = loadConfig(schema, {
  exitOnError: false, // Don't exit process
  onError: (errors) => {
    // Custom logging
    logger.error('Configuration validation failed:', errors);
    
    // Send to monitoring service
    monitoring.track('config_validation_failed', { errors });
  }
});

Sensitive Data Protection

Sensitive values are automatically masked in error messages and logs:

const config = loadConfig(schema, {
  sensitive: ['API_KEY', 'DATABASE_URL', 'JWT_SECRET']
});

// Error message shows:
// - API_KEY: Invalid format (value: ***)
// Instead of exposing the actual value

console.log(config.toString());
// Output:
// {
//   "PORT": 3000,
//   "API_KEY": "***",
//   "DATABASE_URL": "***"
// }

Testing

Testing Configuration

// test-config.ts
import { loadConfig } from 'type-safe-config-loader';
import { testSchema } from '../test-helpers';

describe('Configuration', () => {
  it('should load valid config', () => {
    process.env.PORT = '3000';
    process.env.DATABASE_URL = 'postgres://localhost:5432/test';
    
    const config = loadConfig(testSchema);
    
    expect(config.PORT).toBe(3000);
    expect(config.DATABASE_URL).toBe('postgres://localhost:5432/test');
  });
  
  it('should fail with invalid config', () => {
    process.env.PORT = 'invalid';
    
    expect(() => loadConfig(testSchema, { exitOnError: false }))
      .toThrow('Config validation failed');
  });
});

Test Helpers

// test-helpers.ts
import { createTestConfig } from 'type-safe-config-loader/testing';

export const testConfig = createTestConfig(mySchema, {
  PORT: 3000,
  DATABASE_URL: 'postgres://localhost:5432/test',
  API_KEY: 'test-api-key'
});

// Use in tests
it('should handle user creation', () => {
  const result = createUser(testConfig);
  expect(result).toBeDefined();
});

Performance

Type-Safe Config Loader is designed for minimal startup overhead:

  • Validation: ~1-2ms for typical schemas (< 50 fields)
  • File Loading: Minimal I/O with efficient parsing
  • Memory: < 1MB additional memory usage
  • Bundle Size: ~50KB (including Zod dependency)

Performance is measured at application startup only - no runtime overhead.

Integrations

Express.js

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

const app = express();

app.listen(config.server.port, () => {
  console.log(`Server running on http://${config.server.host}:${config.server.port}`);
});

Fastify

import Fastify from 'fastify';
import { config } from './config';

const fastify = Fastify({
  logger: config.LOG_LEVEL !== 'debug'
});

await fastify.listen({ 
  port: config.server.port, 
  host: config.server.host 
});

NestJS

// config.service.ts
import { Injectable } from '@nestjs/common';
import { config } from './config';

@Injectable()
export class ConfigService {
  get database() {
    return config.database;
  }
  
  get server() {
    return config.server;
  }
}

Docker

# Dockerfile
ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_URL=postgres://db:5432/myapp

COPY config.production.yaml ./config.yaml
# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://db:5432/myapp
      - API_KEY=${API_KEY}
    volumes:
      - ./config.production.yaml:/app/config.yaml

Migration Guide

From dotenv

// Before
require('dotenv').config();
const port = parseInt(process.env.PORT || '3000');
const dbUrl = process.env.DATABASE_URL || '';

// After
import { loadConfig, defineConfig } from 'type-safe-config-loader';
import { z } from 'zod';

const schema = defineConfig({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
});

const config = loadConfig(schema, { dotenv: true });

From node-config

// Before
import config from 'config';
const port = config.get('server.port');

// After
import { loadConfig, defineConfig } from 'type-safe-config-loader';

const schema = defineConfig({
  server: z.object({
    port: z.coerce.number().default(3000)
  })
});

const config = loadConfig(schema, { file: 'config.yaml' });
const port = config.server.port; // Fully typed!

From convict

// Before
const convict = require('convict');
const config = convict({
  port: {
    doc: 'The port to bind.',
    format: 'port',
    default: 3000,
    env: 'PORT'
  }
});

// After
const schema = defineConfig({
  port: z.coerce.number().int().min(1).max(65535).default(3000)
});

const config = loadConfig(schema, { dotenv: true });

Troubleshooting

Common Issues

1. TypeScript errors with inferred types

// Problem: Type inference not working
const config = loadConfig(schema);
// config is 'any'

// Solution: Ensure proper schema typing
const schema = defineConfig({
  PORT: z.coerce.number().default(3000)
});
// Now config.PORT is properly typed as number

2. Environment variables not loading

// Problem: .env file not found
const config = loadConfig(schema, { dotenv: true });

// Solution: Check .env file location
const config = loadConfig(schema, { 
  dotenv: '.env.local' // Specify custom path
});

3. Nested object validation failing

// Problem: Flat env vars for nested config
// DATABASE_HOST=localhost doesn't map to config.database.host

// Solution: Use nested schema structure
const schema = defineConfig({
  database: z.object({
    host: z.string().default('localhost')
  })
});

// Set env var as: DATABASE_HOST=localhost

4. File loading errors

// Problem: Config file not found
const config = loadConfig(schema, { 
  file: 'missing-config.yaml',
  strict: true // This will throw error if file missing
});

// Solution: Make file optional or check existence
const config = loadConfig(schema, { 
  file: 'config.yaml',
  strict: false // Allow missing files
});

Debug Mode

Enable debug logging to troubleshoot configuration loading:

const config = loadConfig(schema, {
  file: 'config.yaml',
  dotenv: true,
  onError: (errors) => {
    console.log('Config validation errors:', errors);
  }
});

// Use schema printer for documentation
console.log(printConfigSchema(schema));

Best Practices

1. Schema Organization

// ✅ Good: Organize by domain
const schema = defineConfig({
  server: z.object({
    port: z.coerce.number().default(3000),
    host: z.string().default('localhost'),
  }),
  
  database: z.object({
    url: z.string().url(),
    pool: z.object({
      min: z.coerce.number().default(2),
      max: z.coerce.number().default(10),
    }),
  }),
  
  cache: z.object({
    enabled: z.coerce.boolean().default(true),
    ttl: z.coerce.number().default(3600),
  }),
});

// ❌ Avoid: Flat structure for complex apps
const schema = defineConfig({
  SERVER_PORT: z.coerce.number().default(3000),
  SERVER_HOST: z.string().default('localhost'),
  DATABASE_URL: z.string().url(),
  DATABASE_POOL_MIN: z.coerce.number().default(2),
  // ... gets unwieldy quickly
});

2. Environment-Specific Defaults

const schema = defineConfig({
  logLevel: z.enum(['debug', 'info', 'warn', 'error'])
    .default(process.env.NODE_ENV === 'development' ? 'debug' : 'info'),
  
  database: z.object({
    ssl: z.coerce.boolean()
      .default(process.env.NODE_ENV === 'production'),
  }),
});

3. Validation Rules

// ✅ Good: Specific validation
const schema = defineConfig({
  port: z.coerce.number().int().min(1).max(65535),
  email: z.string().email(),
  url: z.string().url(),
  
  // Custom validation
  apiKey: z.string().min(32, 'API key must be at least 32 characters'),
});

// ❌ Avoid: Generic validation
const schema = defineConfig({
  port: z.coerce.number(), // Could be negative or too large
  email: z.string(), // Could be invalid email
  url: z.string(), // Could be invalid URL
});

4. Sensitive Data Handling

// ✅ Good: Mark all sensitive fields
const config = loadConfig(schema, {
  sensitive: [
    'database.password',
    'apiKey',
    'jwtSecret',
    'oauth.clientSecret'
  ]
});

// ✅ Good: Use specific env vars for secrets
// In production, set via secure env injection
// In development, use .env with .gitignore

5. Error Handling

// ✅ Good: Fail fast in production
const config = loadConfig(schema, {
  exitOnError: process.env.NODE_ENV === 'production',
  onError: (errors) => {
    if (process.env.NODE_ENV === 'development') {
      console.log('\n📋 Expected configuration:');
      console.log(printConfigSchema(schema));
    }
  }
});

Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

# Clone repository
git clone https://github.com/lanemc/type-safe-config-loader.git
cd type-safe-config-loader

# Install dependencies
npm install

# Run tests
npm test

# Build
npm run build

# Type check
npm run typecheck

# Lint
npm run lint

Running Tests

# Run all tests
npm test

# Watch mode
npm run test:watch

# Coverage report
npm run test:coverage

License

MIT © Lane McGregor

Changelog

See CHANGELOG.md for version history.


Ready to eliminate configuration bugs? Install Type-Safe Config Loader today and never worry about runtime config errors again.

npm install type-safe-config-loader zod

For questions, issues, or feature requests, please visit our GitHub repository.