@rawnodes/config-loader
v1.7.0
Published
Flexible YAML config loader with environment overrides, Zod validation, and Docker-friendly features
Maintainers
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-loaderFor Zod validation (optional):
pnpm add zodQuick 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); // 3000Configuration 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 setOptional 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 undefinedHow stripEmpty works:
""(empty string) →undefined- Objects with all
undefinedvalues → 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 valueimport { 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 ofloadConfig()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 valueExamples:
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 ofloadConfig()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: 8080Files 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 setUsage 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
