.env config loader with validation and defaults
npm install @technomoron/env-loaderA robust, minimal-dependency utility for loading, validating, and parsing environment variables with .env file support, strong type inference, and advanced validation powered by Zod or custom transforms.
- Minimal dependencies: Only Zod is required for advanced validation.
- Type-safe: Full TypeScript type inference from your schema.
- Zod validation: Native support for Zod schemas and custom transforms.
- Flexible parsing: Supports string, number, boolean, strings (comma lists), enums, and custom logic.
- Layered config: Optionally merge/override with multiple .env files.
- Duplicate detection: Detects duplicate keys (case-insensitive) within a single .env file and warns if debug: true.
- Strict mode: Optional proxy that throws on unknown config keys.
- Configurable: Debug logging, custom search paths and filenames, and more.
- Template generation: Auto-generate a commented .env template from your schema (with optional grouped headers).
```
npm install @technomoron/env-loaderor
yarn add @technomoron/env-loaderor
pnpm add @technomoron/env-loader
`
import EnvLoader, { defineEnvOptions, envConfig } from '@technomoron/env-loader';
import { z } from 'zod';
// Define your schema using defineEnvOptions for best type inference
const envOptions = defineEnvOptions({
NODE_ENV: {
description: 'Runtime environment',
options: ['development', 'production', 'test'],
default: 'development',
},
PORT: {
description: 'Server port',
type: 'number',
default: 3000,
},
DATABASE_URL: {
description: 'Database connection string',
required: true,
},
FEATURE_FLAGS: {
description: 'Comma-separated features',
type: 'strings',
default: ['logging', 'metrics'],
},
ENABLE_EMAIL: {
description: 'Enable email service',
type: 'boolean',
default: false,
},
LOG_LEVEL: {
description: 'Log verbosity',
options: ['error', 'warn', 'info', 'debug'],
default: 'info',
},
CUSTOM: {
description: 'Custom value validated by Zod',
zodSchema: z.string().regex(/^foo-.+/),
default: 'foo-bar',
},
});
// Type-safe config for VSCode/IDE
const config = EnvLoader.createConfig(envOptions);
// Now use your config!
console.log(Running in ${config.NODE_ENV} mode on port ${config.PORT});Features: ${config.FEATURE_FLAGS.join(', ')}
console.log();Email enabled? ${config.ENABLE_EMAIL}
console.log();Custom value: ${config.CUSTOM}
console.log();`
---
Helper for TypeScript type inference. Pass your env schema as an object. Optional meta lets you apply a group label to all options (used for template headers).
- description (string): What this variable is for.type
- (string | number | boolean | strings): Parsing mode.required
- (boolean): Whether it must be present.options
- (array): Valid values (enum-like).default
- : Fallback if not set. (Type matches type)transform
- (function): Custom parser, (raw: string) => any.zodSchema
- (ZodType): Full validation/transformation via Zod.group
- (string): Optional grouping label for template generation.
Loads, parses, and validates environment using your schema.
- envOptions: Your schema (from defineEnvOptions).options
- : Loader options (see below).
Returns: Typed config object where all keys are the inferred types.
Same as createConfig, but returned config throws on unknown keys (useful for strict/safer code).
Generate a commented .env template file (with descriptions and default/example values) for your schema.
``
EnvLoader.genTemplate(envOptions, '.env.example');
You can group sections by supplying group on options or via the optional meta argument to defineEnvOptions:
`
const serverEnv = defineEnvOptions(
{
PORT: { description: 'Port number', type: 'number', default: 3000 },
LOG_LEVEL: { description: 'Log level', options: ['info', 'debug'], default: 'info' },
},
{ group: 'MAIN SERVER' }
);
const jwtEnv = defineEnvOptions(
{
JWT_SECRET: { description: 'Signing secret', required: true },
JWT_TTL: { description: 'Expiry seconds', type: 'number', default: 3600 },
},
{ group: 'JWT TOKEN STORE' }
);
EnvLoader.genTemplate({ ...serverEnv, ...jwtEnv }, '.env.example');
`
Template output will include # MAIN SERVER and # JWT TOKEN STORE headers; if no group is provided, output matches prior behavior.
You can also pass multiple blocks without spreading using EnvLoader.genTemplateFromBlocks([serverEnv, jwtEnv], '.env.example');.
---
EnvLoader can be subclassed if you want to override internals like file discovery. The static factories now instantiate the subclass, and helper methods are protected:
`
class BaseEnv extends EnvLoader {
static schema = defineEnvOptions({ PORT: { type: 'number', required: true } });
}
class AppEnv extends BaseEnv {
static schema = defineEnvOptions({
...BaseEnv.schema,
HOST: { required: true },
});
protected override loadEnvFiles() {
const base = super.loadEnvFiles();
return { ...base, HOST: base.HOST ?? '127.0.0.1' };
}
}
const config = AppEnv.createConfig(AppEnv.schema, { merge: true });
`
---
Pass as second argument to createConfig, createConfigProxy, or in the constructor:
- searchPaths (string[]): Folders to search for .env files. Default: ['./']fileNames
- (string[]): Filenames to load. Default: ['.env']merge
- (boolean): If true, merge all found files (last wins). Default: false (alias: cascade)debug
- (boolean): Print debug output and duplicate key warnings. Default: falseenvFallback
- (boolean): Fallback to process.env if not found in files. Default: true
---
- string: Default if type is omitted.Number()
- number: Parsed using .true
- boolean: Accepts , false, 1, 0, yes, no, on, off (case-insensitive).
- strings: Comma-separated list → string array.
Use options: [...] to enforce one of several allowed values.
Use zodSchema for advanced parsing/validation:
`
import { z } from 'zod';
const schema = defineEnvOptions({
SECRET: {
description: 'Must start with foo-',
zodSchema: z.string().startsWith('foo-'),
required: true,
},
});
`
Use transform: (raw) => parsed for custom logic:
``
const schema = defineEnvOptions({
PORT: {
description: 'Server port',
transform: (v) => parseInt(v) + 1000, // e.g., offset
default: '3000',
},
});
---
- Missing required keys:
``
Missing from config: DATABASE_URL,API_KEY
- Type errors:
``
'PORT' must be a number
'ENABLE_EMAIL' must be a boolean
- Zod schema failures:
``
'CUSTOM' zod says it bad: Invalid input
- Invalid options:
``
Invalid 'LOG_LEVEL': silly
- Duplicate keys in .env (with debug enabled):
``
Duplicate keys in .env: FOO (lines 1 and 3), bar (lines 5 and 8)
All validation errors are thrown as a single error message (one per line).
---
Load multiple .env files (e.g. for local overrides or per-environment):
`.env.${env}
const config = EnvLoader.createConfig(envOptions, {
searchPaths: ['./'],
fileNames: (() => {
const base = ['.env', '.env.local'];
const env = process.env.NODE_ENV;
if (env) base.push();`
return base;
})(),
merge: true,
});
Load order:
1. .env (base).env.local
2. (overrides).env.production
3. (if NODE_ENV=production)
---
For extra safety, use the proxy:
`
const config = EnvLoader.createConfigProxy(envOptions);
console.log(config.PORT); // ok
console.log(config.UNKNOWN); // throws Error!
`
---
files.env
``
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost/db
FEATURE_FLAGS=logging,metrics
ENABLE_EMAIL=false
.env.local
``
DATABASE_URL=postgresql://dev:password@localhost/devdb
ENABLE_EMAIL=true
.env.production
``
NODE_ENV=production
PORT=8080
LOG_LEVEL=warn
---
- API is static/class-based (EnvLoader.createConfig, not instance .define/.validate)..env
- Supports advanced parsing with Zod or custom transforms.
- Built-in template/documentation generator: EnvLoader.genTemplate(...)..env` file are detected and warned about in debug mode.
- Duplicate keys in a single
---
MIT - Copyright (c) 2025 Bjørn Erik Jacobsen / Technomoron