No description
Find a file
autocommit 21af0465ce
Some checks failed
Build and Publish / build-and-publish (push) Failing after 43s
chore: bump version to 1.0.28
2026-04-12 11:30:24 -07:00
.forgejo/workflows chore: 🔧 Update files 2026-01-15 06:53:01 -08:00
.githooks feat: add GitLab npm publishing config 2025-12-29 21:36:33 -08:00
src 🔧 update imports to @lilith namespace 2025-12-31 01:32:37 -08:00
.gitignore Initial commit: @transquinnftw/yaml-config v1.0.0 2025-12-28 03:37:25 -08:00
.npmignore Initial commit: @transquinnftw/yaml-config v1.0.0 2025-12-28 03:37:25 -08:00
ARCHITECTURE.md Initial commit: @transquinnftw/yaml-config v1.0.0 2025-12-28 03:37:25 -08:00
example.ts 🔧 update imports to @lilith namespace 2025-12-31 01:32:37 -08:00
package.json chore: bump version to 1.0.28 2026-04-12 11:30:24 -07:00
README.md chore: re-trigger CI publish 2026-01-30 13:47:27 -08:00
tsconfig.json chore(shared): 🔧 Update shared configuration files and scripts 2026-01-16 20:52:28 -08:00
tsup.config.ts build: 🔧 Optimize bundling configuration for reduced build artifacts size and improved performance 2026-01-21 15:36:44 -08:00

@transquinnftw/yaml-config

Type-safe YAML configuration loader with Zod validation, environment variable overrides, hot reload support, and variable interpolation.

Features

  • Type Safety: Full TypeScript support with Zod schema validation
  • Variable Interpolation: Use ${variable.path} syntax in YAML files
  • Path Expansion: Automatic ~ expansion to home directory
  • Environment Overrides: Override any config value via environment variables
  • Hot Reload: Optional file watching for automatic config reloading
  • Deep Merging: Intelligent merging of nested configuration objects
  • Fallback Paths: Search multiple locations for config files
  • Validation Errors: Clear, formatted error messages for invalid configs

Installation

pnpm add @transquinnftw/yaml-config

Quick Start

import { createConfigLoader, z } from '@transquinnftw/yaml-config';

// Define your config schema
const schema = z.object({
  port: z.number().default(8000),
  database: z.object({
    host: z.string(),
    port: z.number(),
    url: z.string(), // Can use interpolation in YAML
  }),
});

// Create loader
const loader = createConfigLoader({
  path: './config.yaml',
  schema,
  envPrefix: 'APP_',
});

// Load config
const config = loader.load();

// Type-safe access
console.log(config.port); // number
console.log(config.database.host); // string

YAML Configuration

# config.yaml
port: 8000

database:
  host: localhost
  port: 5432
  # Variable interpolation
  url: "postgresql://${database.host}:${database.port}/mydb"

Environment Variable Overrides

With envPrefix: 'APP_', you can override any config value:

APP_PORT=9000 node app.js
APP_DATABASE_HOST=prod.example.com node app.js
APP_DATABASE_PORT=5433 node app.js

The loader automatically:

  • Converts env var names to config paths (underscores to dots)
  • Coerces types based on schema (strings to numbers/booleans)
  • Deep merges with file-based config

Advanced Usage

Hot Reload

const loader = createConfigLoader({
  path: './config.yaml',
  schema,
  watch: true,
  onReload: (newConfig) => {
    console.log('Config reloaded:', newConfig);
    // Update application state
  },
});

const config = loader.load();

// Later: stop watching
loader.stopWatching();

Fallback Paths

const loader = createConfigLoader({
  path: './config.yaml',
  fallbackPaths: [
    '/etc/myapp/config.yaml',
    '~/.config/myapp/config.yaml',
  ],
  schema,
});

The loader will search paths in order and use the first one found.

Path Expansion

Any key named path or ending with Path will have ~ automatically expanded:

# config.yaml
service:
  logPath: "~/logs/app.log"  # Expands to /home/user/logs/app.log
  dataPath: "~/data"          # Expands to /home/user/data

Common Schema Patterns

import { z, commonSchemas } from '@transquinnftw/yaml-config';

const schema = z.object({
  // Use built-in patterns
  port: commonSchemas.port,              // 1-65535
  apiUrl: commonSchemas.url,             // Valid URL
  email: commonSchemas.email,            // Valid email
  dbPath: commonSchemas.filePath,        // Non-empty string

  // Or define custom
  logLevel: z.enum(['debug', 'info', 'warn', 'error']),
  retries: z.number().int().min(0).max(10),
});

Error Handling

const loader = createConfigLoader({
  path: './config.yaml',
  schema,
  onError: (error) => {
    if (error instanceof ValidationError) {
      console.error('Validation failed:');
      console.error(error.formatErrors());
    } else {
      console.error('Failed to load config:', error.message);
    }
    // Use defaults or exit
    process.exit(1);
  },
});

Manual Reload

const loader = createConfigLoader({ path: './config.yaml', schema });

const config = loader.load();

// Later: reload without watching
const updatedConfig = loader.reload();

// Or: get cached config without reloading
const cached = loader.get(); // Returns null if not loaded yet

API Reference

createConfigLoader(options)

Creates a new configuration loader instance.

Options:

  • path: string - Path to YAML config file (required)
  • schema: ZodSchema - Zod schema for validation (required)
  • envPrefix?: string - Prefix for environment variable overrides
  • fallbackPaths?: string[] - Additional paths to search
  • watch?: boolean - Enable file watching (default: false)
  • onReload?: (config) => void - Callback on reload
  • log?: (message) => void - Custom log function (default: console.log)
  • onError?: (error) => void - Custom error handler

Returns: ConfigLoader instance

ConfigLoader

  • load(): Config - Load and validate configuration
  • reload(): Config - Reload configuration from disk
  • get(): Config | null - Get cached config without reloading
  • stopWatching(): void - Stop file watching
  • getConfigPath(): string | null - Get resolved config file path

commonSchemas

Pre-built Zod schemas for common config patterns:

  • url - Valid URL
  • port - Port number (1-65535)
  • filePath - Non-empty file path
  • dirPath - Non-empty directory path
  • email - Valid email address
  • nonEmptyString - Non-empty string
  • positiveInt - Positive integer
  • nonNegativeInt - Non-negative integer
  • booleanFromString - Coerce string to boolean
  • numberFromString - Coerce string to number

ValidationError

Extended Error class for validation failures:

  • errors: ZodError - Original Zod error
  • formatErrors(): string - Format errors as human-readable string

Examples

Microservice Configuration

import { createConfigLoader, z, commonSchemas } from '@transquinnftw/yaml-config';

const schema = z.object({
  service: z.object({
    name: z.string(),
    port: commonSchemas.port,
    host: z.string().default('0.0.0.0'),
  }),
  database: z.object({
    host: z.string(),
    port: commonSchemas.port,
    name: z.string(),
    url: z.string(), // Constructed via interpolation
  }),
  redis: z.object({
    host: z.string(),
    port: commonSchemas.port,
    url: z.string(),
  }),
  logging: z.object({
    level: z.enum(['debug', 'info', 'warn', 'error']),
    path: commonSchemas.filePath,
  }),
});

const loader = createConfigLoader({
  path: './config.yaml',
  schema,
  envPrefix: 'SERVICE_',
  fallbackPaths: [
    '/etc/myservice/config.yaml',
  ],
});

export const config = loader.load();
# config.yaml
service:
  name: "my-service"
  port: 8000
  host: "0.0.0.0"

database:
  host: "localhost"
  port: 5432
  name: "mydb"
  url: "postgresql://${database.host}:${database.port}/${database.name}"

redis:
  host: "localhost"
  port: 6379
  url: "redis://${redis.host}:${redis.port}"

logging:
  level: "info"
  path: "~/logs/service.log"

Desktop Application

import { createConfigLoader, z } from '@transquinnftw/yaml-config';

const schema = z.object({
  app: z.object({
    name: z.string(),
    version: z.string(),
  }),
  window: z.object({
    width: z.number().default(1200),
    height: z.number().default(800),
    minWidth: z.number().default(800),
    minHeight: z.number().default(600),
  }),
  theme: z.enum(['light', 'dark', 'system']).default('system'),
  dataPath: z.string(),
});

const loader = createConfigLoader({
  path: './config.yaml',
  schema,
  watch: true,
  onReload: (config) => {
    // Update app theme when config changes
    updateTheme(config.theme);
  },
});

export const config = loader.load();

License

MIT

Author

Quinn quinn@transquinnftw.dev

Test 1767646849