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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rawnodes/config-loader

v1.7.0

Published

Flexible YAML config loader with environment overrides, Zod validation, and Docker-friendly features

Readme

@rawnodes/config-loader

Flexible YAML configuration loader for Node.js applications with environment overrides, Zod validation, and Docker-friendly features.

Features

  • YAML Configuration - Load config from YAML files with environment-specific overrides
  • Environment Variables - Replace ${VAR} placeholders with env values
  • HashiCorp Vault - Read secrets from Vault using ${vault:path:field} syntax
  • AWS Secrets Manager - Read secrets from AWS using ${aws:secret-name:field} syntax
  • Zod Validation - Optional schema validation with detailed error messages
  • Docker/K8s Ready - Mount additional config files via overrideDir
  • Secret Masking - Automatic masking of sensitive values in logs
  • TypeScript First - Full type safety with generics

Installation

pnpm add @rawnodes/config-loader
# or
npm install @rawnodes/config-loader

For Zod validation (optional):

pnpm add zod

Quick Start

1. Create config files

# config/base.yml
server:
  port: 3000
  host: localhost

database:
  host: localhost
  port: 5432
  name: myapp
# config/production.yml
server:
  host: 0.0.0.0

database:
  host: ${DATABASE_HOST}
  password: ${DATABASE_PASSWORD}

2. Load configuration

import { loadConfig } from '@rawnodes/config-loader';

interface AppConfig {
  server: { port: number; host: string };
  database: { host: string; port: number; name: string; password?: string };
}

const { config } = loadConfig<AppConfig>({
  configDir: './config',
  dotenv: true,
});

console.log(config.server.port); // 3000

Configuration Options

interface ConfigLoaderOptions<T> {
  // Directory with config files (default: process.cwd())
  configDir?: string;

  // Base config filename without extension (default: 'base')
  baseFileName?: string;

  // Environment name (default: process.env.NODE_ENV || 'local')
  environment?: string;

  // File extension (default: 'yml')
  extension?: 'yml' | 'yaml';

  // Custom post-processing function
  postProcess?: (config: T) => T;

  // Zod schema for validation
  schema?: z.ZodType<T>;

  // Logger callback (config will be logged with masked secrets)
  logger?: (message: string) => void;

  // Load .env file (default: false)
  dotenv?: boolean | { path?: string };

  // Directory with additional YAML files to merge (default: '/etc/app/config')
  // Set to false to disable
  overrideDir?: string | false;

  // Remove empty strings and empty objects from config (default: false)
  // Useful for optional fields with ${VAR:} placeholders
  stripEmpty?: boolean;

  // HashiCorp Vault options (requires loadConfigAsync)
  vault?: {
    endpoint: string;     // Vault server URL
    roleId: string;       // AppRole role_id
    secretId: string;     // AppRole secret_id
    namespace?: string;   // Optional Vault Enterprise namespace
  };

  // AWS Secrets Manager options (requires loadConfigAsync)
  aws?: {
    region: string;          // AWS region
    accessKeyId: string;     // AWS access key ID
    secretAccessKey: string; // AWS secret access key
  };
}

Environment Variables

Use ${VAR} syntax with optional defaults:

database:
  host: ${DB_HOST:localhost}
  port: ${DB_PORT:5432}
  password: ${DB_PASSWORD}  # Required - throws if not set

Optional Fields

Use ${VAR:} (empty default) with stripEmpty: true to support optional fields:

# config/base.yml
server:
  port: 3000

monitoring:
  serviceId: ${MONITORING_SERVICE_ID:}
  apiKey: ${MONITORING_API_KEY:}
  healthcheckUrls:
    cleanup: ${MONITORING_CLEANUP_URL:}
    sync: ${MONITORING_SYNC_URL:}
const schema = z.object({
  server: z.object({ port: z.number() }),
  monitoring: z.object({
    serviceId: z.string(),
    apiKey: z.string(),
    healthcheckUrls: z.object({
      cleanup: z.string().url().optional(),
      sync: z.string().url().optional(),
    }).optional(),
  }).optional(),
});

const { config } = loadConfig({
  schema,
  stripEmpty: true,  // Empty strings → undefined, empty objects removed
});

// If no MONITORING_* env vars are set, config.monitoring will be undefined

How stripEmpty works:

  • "" (empty string) → undefined
  • Objects with all undefined values → removed
  • Arrays: empty strings filtered out
  • Other falsy values (0, false, null) are preserved

HashiCorp Vault Integration

Read secrets directly from HashiCorp Vault using AppRole authentication.

Setup

# config/base.yml
database:
  host: localhost
  password: ${vault:secret/data/api:DB_PASSWORD}
  port: ${vault:secret/data/api:DB_PORT:5432}  # with default value
import { loadConfigAsync } from '@rawnodes/config-loader';

const { config } = await loadConfigAsync({
  configDir: './config',
  vault: {
    endpoint: 'https://vault.example.com',
    roleId: process.env.VAULT_ROLE_ID!,
    secretId: process.env.VAULT_SECRET_ID!,
    namespace: 'optional-namespace',  // for Vault Enterprise
  },
});

Syntax

${vault:PATH:FIELD}           - Required secret
${vault:PATH:FIELD:DEFAULT}   - With default value (used if secret not found)

Examples:

database:
  # Read DB_PASSWORD from secret/data/api
  password: ${vault:secret/data/api:DB_PASSWORD}

  # With default value (supports colons in default)
  url: ${vault:secret/data/db:URL:postgres://localhost:5432/app}

  # Mix with env variables
  host: ${DB_HOST:localhost}

Notes

  • Use loadConfigAsync() instead of loadConfig() when using Vault
  • Secrets are cached per path during config load (multiple fields from same path = 1 API call)
  • Supports Vault KV v1 and v2 secret engines

AWS Secrets Manager Integration

Read secrets from AWS Secrets Manager.

Setup

# config/base.yml
database:
  password: ${aws:my-app/database:DB_PASSWORD}
  host: ${aws:my-app/database:DB_HOST:localhost}
api:
  key: ${aws:my-app/api-key}  # plain string secret (no field)
import { loadConfigAsync } from '@rawnodes/config-loader';

const { config } = await loadConfigAsync({
  configDir: './config',
  aws: {
    region: 'us-east-1',
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

Syntax

${aws:SECRET_NAME}              - Plain string secret (entire value)
${aws:SECRET_NAME:FIELD}        - JSON secret, extract field
${aws:SECRET_NAME:FIELD:DEFAULT} - With default value

Examples:

database:
  # JSON secret: {"DB_PASSWORD": "secret", "DB_USER": "admin"}
  password: ${aws:my-app/database:DB_PASSWORD}
  user: ${aws:my-app/database:DB_USER}

  # Plain string secret (no field)
  api_key: ${aws:my-app/api-key}

  # With default value
  host: ${aws:my-app/database:DB_HOST:localhost}

Notes

  • Use loadConfigAsync() instead of loadConfig() when using AWS
  • Secrets are cached per secret name during config load
  • Supports both JSON and plain string secrets

Zod Validation

import { z } from 'zod';
import { loadConfig } from '@rawnodes/config-loader';

const AppConfigSchema = z.object({
  server: z.object({
    port: z.number().min(1).max(65535),
    host: z.string(),
  }),
  database: z.object({
    host: z.string(),
    port: z.number(),
    name: z.string(),
    password: z.string().optional(),
  }),
});

type AppConfig = z.infer<typeof AppConfigSchema>;

const { config } = loadConfig<AppConfig>({
  schema: AppConfigSchema,
  logger: console.log,
});

Docker/Kubernetes Override

Mount additional config files in /etc/app/config/:

# /etc/app/config/01-secrets.yml
database:
  password: super-secret

# /etc/app/config/02-overrides.yml
server:
  port: 8080

Files are merged in alphabetical order.

Environment Helpers

env

Environment detection helpers:

import { env } from '@rawnodes/config-loader';

env.nodeEnv       // process.env.NODE_ENV || 'local'
env.isProduction  // true if NODE_ENV === 'production'
env.isDevelopment // true if NODE_ENV === 'development'
env.isTest        // true if NODE_ENV === 'test'
env.isLocal       // true if NODE_ENV is 'local' or not set

Usage with Vault/AWS

import { loadConfigAsync, env } from '@rawnodes/config-loader';

const { config } = await loadConfigAsync({
  configDir: './config',
  vault: !env.isLocal && {
    endpoint: process.env.VAULT_ENDPOINT,
    roleId: process.env.VAULT_ROLE_ID,
    secretId: process.env.VAULT_SECRET_ID,
  },
});
// If any value is missing, you'll get a clear error:
// "Vault endpoint is not set" or "Vault roleId is not set"

NestJS Integration

// config.module.ts
import { Module, Global } from '@nestjs/common';
import { loadConfig } from '@rawnodes/config-loader';
import { AppConfigSchema, AppConfig } from './app.config';

const { config } = loadConfig<AppConfig>({
  schema: AppConfigSchema,
  dotenv: true,
});

@Global()
@Module({
  providers: [
    {
      provide: 'CONFIG',
      useValue: config,
    },
  ],
  exports: ['CONFIG'],
})
export class ConfigModule {}

API

loadConfig<T>(options?): ConfigLoaderResult<T>

Loads and merges configuration files synchronously.

Returns:

interface ConfigLoaderResult<T> {
  config: T;           // Loaded configuration
  environment: string; // Resolved environment name
  configDir: string;   // Resolved config directory path
}

loadConfigAsync<T>(options?): Promise<ConfigLoaderResult<T>>

Async version with Vault support. Required when using vault option.

const { config } = await loadConfigAsync({
  configDir: './config',
  vault: {
    endpoint: 'https://vault.example.com',
    roleId: 'role-id',
    secretId: 'secret-id',
  },
});

maskSecrets(obj): unknown

Masks sensitive values in an object. Useful for logging.

import { maskSecrets } from '@rawnodes/config-loader';

const masked = maskSecrets({
  user: 'admin',
  password: 'secret123',
  url: 'postgres://user:pass@localhost/db',
});
// { user: 'admin', password: 'se***23', url: 'postgres://user:***@localhost/db' }

deepMerge(base, override): object

Deep merges two objects.

replacePlaceholders(obj): unknown

Replaces ${VAR} placeholders with environment variable values.

stripEmpty(obj): unknown

Removes empty strings and empty objects recursively. Useful for cleaning up config before validation.

import { stripEmpty } from '@rawnodes/config-loader';

const cleaned = stripEmpty({
  server: { port: 3000 },
  optional: { url: '', name: '' },
});
// { server: { port: 3000 } }

License

MIT