@systemix/env
Typed environment variable loading and validation. Zero dependencies. Built-in .env file support. Requires Node.js 24.7+.
Installation
pnpm add @systemix/env
Basic Usage
import { load } from '@systemix/env'; const env = load({ NODE_ENV: { type: 'string', default: 'development' }, PORT: { type: 'number', default: 3000 }, DATABASE_URL: { type: 'string', required: true, secret: true }, DEBUG: { type: 'boolean', default: false }, }); console.log(env.PORT); // number console.log(env.toSafeLog()); // { DATABASE_URL: '***', ... }
Schema
| Property | Type | Description |
|---|---|---|
type | 'string' | 'number' | 'boolean' | How to parse the value |
required | boolean | If true, throws when missing |
default | string | number | boolean | Used when var is missing or empty |
min | number | Min value (number type) |
max | number | Max value (number type) |
regex | RegExp | Must match (string type) |
oneOf | string[] | Allowed values (string type) |
transform | (raw) => value | Custom parser |
secret | boolean | Masked in toSafeLog() |
Options
load(schema, { source: { PORT: '4000' }, // env source (default: process.env) fromFile: ['.env', '.env.local'], // .env file(s); later files override earlier ones strict: true, // error on keys in a .env file that are not in the schema });
Precedence
Highest first: source (the real environment), then fromFile files (later over earlier), then schema defaults. A .env file never overrides a variable that is really set, so a stray .env in production cannot replace deployed secrets. Missing files are skipped.
strict
With strict: true, a .env file that contains a key the schema doesn't know is an error, which catches typos like DATABSE_URL that would otherwise be silently ignored. Unrelated variables in the real environment are not flagged.
Standard Schema (Zod, Valibot, ArkType)
Any Standard Schema validator can validate a variable. It receives the raw string, so coercing schemas work, and the result type is the schema's output type:
import { z } from 'zod'; const env = load({ PORT: { schema: z.coerce.number().int().min(1).max(65535), default: 3000 }, DATABASE_URL: { schema: z.url(), required: true, secret: true }, }); env.PORT; // number env.DATABASE_URL; // string
required, default (checked against the output type) and secret work the same as for built-in types. Validators must be synchronous; an async one fails with a clear error. This package doesn't depend on any validator library.
.env Files
const env = load(schema, { fromFile: '.env.local' });
The parser (also exported as parseEnvFile) supports:
KEY=valuewith an optionalexportprefix, and#comment lines- inline comments:
KEY=value # note(a#with no space before it, as in URLs, is kept) "double quotes": may span lines;\n \r \t \" \\are expanded, other backslashes kept'single quotes': literal, may span lines- invalid keys and lines without
=are skipped
import { parseEnvFile } from '@systemix/env'; const vars = parseEnvFile(readFileSync('.env', 'utf-8'));
Secret Masking
Mark sensitive vars with secret: true:
const env = load( { API_KEY: { type: 'string', secret: true }, PORT: { type: 'number' }, }, { source: { API_KEY: 'sk-123', PORT: '3000' } }, ); env.API_KEY; // 'sk-123' — still readable JSON.stringify(env); // '{"PORT":3000}' — secrets are not enumerable console.log(env); // secrets don't appear env.toSafeLog(); // { API_KEY: '***', PORT: 3000 }
Secrets are non-enumerable (so logging, JSON.stringify and spread don't expose them), never appear in validation errors, and the result object is frozen.
Batch Error Reporting
All validation errors are collected and thrown together:
Env validation failed: - Missing required env var: DATABASE_URL - PORT: must be >= 1
Config (OOP)
Class-based usage with get, getOrThrow, has:
import { Config } from '@systemix/env'; const config = Config.fromEnv({ PORT: { type: 'number', default: 3000 }, DATABASE_URL: { type: 'string', required: true }, }); config.get('PORT'); // number | undefined config.get('PORT', 4000); // number (default when undefined) config.getOrThrow('DATABASE_URL'); // throws if not set config.has('PORT'); // boolean config.toSafeLog(); // masked for logging config.getRaw(); // raw env object
Create from existing env: Config.from(load(schema)).
Number and Boolean Parsing
- Numbers must be decimal (
42,-5,.5,1e3). Empty or whitespace-only input,0x10,Infinityand1_000are rejected. - Booleans (case-insensitive):
true,1,yes,onandfalse,0,no,off. - An empty variable counts as unset: the
defaultapplies, or a required variable is reported missing.
Try it
Open the Env Loader demo to see live examples.