@technomoron/env-loader
v1.1.0
Published
.env config loader with validation and defaults
Readme
@technomoron/env-loader
A 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.
Features
- 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
.envfiles. - Duplicate detection: Detects duplicate keys (case-insensitive) within a single
.envfile and warns ifdebug: 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
.envtemplate from your schema (with optional grouped headers).
Installation
npm install @technomoron/env-loader
# or
yarn add @technomoron/env-loader
# or
pnpm add @technomoron/env-loaderQuick Example
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}`);
console.log(`Features: ${config.FEATURE_FLAGS.join(', ')}`);
console.log(`Email enabled? ${config.ENABLE_EMAIL}`);
console.log(`Custom value: ${config.CUSTOM}`);API
defineEnvOptions(options, meta?)
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).
Environment Option Properties
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 matchestype)transform(function): Custom parser,(raw: string) => any.zodSchema(ZodType): Full validation/transformation via Zod.group(string): Optional grouping label for template generation.
EnvLoader.createConfig(envOptions, options?)
Loads, parses, and validates environment using your schema.
envOptions: Your schema (fromdefineEnvOptions).options: Loader options (see below).
Returns: Typed config object where all keys are the inferred types.
EnvLoader.createConfigProxy(envOptions, options?)
Same as createConfig, but returned config throws on unknown keys (useful for strict/safer code).
EnvLoader.genTemplate(schema, file)
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');.
Subclassing (advanced)
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 });Loader Options
Pass as second argument to createConfig, createConfigProxy, or in the constructor:
searchPaths(string[]): Folders to search for.envfiles. 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 toprocess.envif not found in files. Default:true
Parsing & Validation
Supported Types
- string: Default if
typeis omitted. - number: Parsed using
Number(). - boolean: Accepts
true,false,1,0,yes,no,on,off(case-insensitive). - strings: Comma-separated list → string array.
Enum/Options
Use options: [...] to enforce one of several allowed values.
Zod Schemas
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,
},
});Custom Transform
Use transform: (raw) => parsed for custom logic:
const schema = defineEnvOptions({
PORT: {
description: 'Server port',
transform: (v) => parseInt(v) + 1000, // e.g., offset
default: '3000',
},
});Error Handling & Debugging
Missing required keys:
Missing from config: DATABASE_URL,API_KEYType errors:
'PORT' must be a number 'ENABLE_EMAIL' must be a booleanZod schema failures:
'CUSTOM' zod says it bad: Invalid inputInvalid options:
Invalid 'LOG_LEVEL': sillyDuplicate 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).
Layered Configuration Example
Load multiple .env files (e.g. for local overrides or per-environment):
const config = EnvLoader.createConfig(envOptions, {
searchPaths: ['./'],
fileNames: (() => {
const base = ['.env', '.env.local'];
const env = process.env.NODE_ENV;
if (env) base.push(`.env.${env}`);
return base;
})(),
merge: true,
});Load order:
.env(base).env.local(overrides).env.production(ifNODE_ENV=production)
Advanced: Strict Proxy Mode
For extra safety, use the proxy:
const config = EnvLoader.createConfigProxy(envOptions);
console.log(config.PORT); // ok
console.log(config.UNKNOWN); // throws Error!Example .env 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=warnMigration Notes
- API is static/class-based (
EnvLoader.createConfig, not instance.define/.validate). - Supports advanced parsing with Zod or custom transforms.
- Built-in
.envtemplate/documentation generator:EnvLoader.genTemplate(...). - Duplicate keys in a single
.envfile are detected and warned about in debug mode.
License
MIT - Copyright (c) 2025 Bjørn Erik Jacobsen / Technomoron
