envtrust
v0.3.0
Published
Type-safe environment variable validation. Define your env config as an object, get back a fully typed, validated result.
Downloads
531
Maintainers
Readme
Why envtrust?
| Problem | Solution |
|---|---|
| ❌ process.env.PORT returns string \| undefined | ✅ Get number with full type safety |
| ❌ App crashes at runtime with cryptic errors | ✅ Fail fast at startup with clear messages |
| ❌ No validation — any value passes through | ✅ Built-in validators for common types |
| ❌ Different defaults in dev vs production | ✅ devDefault auto-applies in non-prod |
| ❌ Env objects are mutable and unpredictable | ✅ Returns a frozen, immutable object |
How It Works
process.env cleanEnv() Result
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
│ PORT=3000 │─────▶│ Validate │────▶│ { PORT: 3000 } │ ✅ typed number
│ DEBUG=true│─────▶│ Parse │────▶│ { DEBUG: true } │ ✅ typed boolean
│ API_URL= │─────▶│ Type-check │────▶│ 🚨 Error! │ ❌ missing value
└──────────┘ └──────────────┘ └──────────────────┘Installation
# npm
npm install envtrust
# yarn
yarn add envtrust
# pnpm
pnpm add envtrustZero dependencies. Lightweight and fast.
Quick Start
import { cleanEnv, str, num, bool, url } from 'envtrust';
const env = cleanEnv(process.env, {
NODE_ENV: str({ choices: ['development', 'production', 'test'] }),
PORT: num({ default: 3000 }),
DATABASE_URL: url(),
DEBUG: bool({ default: false }),
API_KEY: str(),
});
// ✅ Fully typed — no more `string | undefined`
env.PORT; // number
env.DEBUG; // boolean
env.DATABASE_URL; // string (validated URL)
env.isProduction; // boolean (auto-generated)
env.isDev; // boolean (auto-generated)
env.isTest; // boolean (auto-generated)Loading .env Files
envtrust includes a built-in loadEnv() — a pure function that loads and layers .env files by profile. Unlike dotenv, it never mutates process.env.
Basic Usage
import { cleanEnv, loadEnv, str, num } from 'envtrust';
const env = cleanEnv(
loadEnv({ profile: 'dev' }),
{
PORT: num({ default: 3000 }),
DB_URL: str(),
},
);File Priority (lowest → highest)
.env ← base defaults
.env.local ← local overrides (gitignored)
.env.{profile} ← profile-specific (e.g. .env.dev, .env.staging, .env.prod)
.env.{profile}.local ← profile + local overrides
process.env ← runtime env always winsExample: Multi-Environment Setup
my-app/
├── .env # PORT=3000, LOG_LEVEL=info
├── .env.local # SECRET_KEY=my-local-secret (gitignored)
├── .env.dev # DB_URL=postgres://localhost/dev, DEBUG=true
├── .env.staging # DB_URL=postgres://staging-host/db
├── .env.prod # DB_URL=postgres://prod-host/db, LOG_LEVEL=error
└── src/
└── env.ts// src/env.ts
import { cleanEnv, loadEnv, str, num, bool, url } from 'envtrust';
const env = cleanEnv(
loadEnv({ profile: process.env.NODE_ENV }), // auto-selects .env.dev / .env.prod etc.
{
PORT: num({ default: 3000 }),
DB_URL: url(),
DEBUG: bool({ default: false }),
LOG_LEVEL: str({ choices: ['debug', 'info', 'warn', 'error'] }),
SECRET_KEY: str(),
},
);
export default env;loadEnv Options
loadEnv({
dir: './config', // directory to look for .env files (default: cwd)
profile: 'staging', // loads .env.staging + .env.staging.local
files: ['custom.env'], // extra files to load (after profile files)
override: process.env, // runtime override (default: process.env)
pure: true, // ignore process.env — only use file values
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| dir | string | process.cwd() | Directory containing .env files |
| profile | string | — | Profile name (dev, staging, prod, etc.) |
| files | string[] | [] | Additional .env file paths to load |
| override | Record | process.env | Runtime values that win over files |
| pure | boolean | false | If true, ignore process.env entirely |
Pure Mode (for testing)
// Isolated — no process.env leaking in
const env = cleanEnv(
loadEnv({ dir: './fixtures', profile: 'test', pure: true }),
{ DB_URL: str(), API_KEY: str() },
);Validators
envtrust ships with 8 built-in validators and a makeValidator helper for custom ones.
| Validator | Output Type | Description |
|-----------|------------|-------------|
| str() | string | Passes through string values |
| num() | number | Parses numeric strings |
| bool() | boolean | Parses boolean-like strings (true, 1, yes, on → true) |
| port() | number | Validates port range 1–65535 |
| url() | string | Validates URL format with protocol & hostname |
| email() | string | Validates email format |
| host() | string | Validates hostname, IPv4, IPv6, or localhost |
| json() | unknown | Parses JSON strings |
Boolean Parsing
Truthy: '1', 'true', 't', 'yes', 'on'
Falsy: '0', 'false', 'f', 'no', 'off'Validator Options
Every validator accepts these options:
{
default?: T; // Default value if env var is missing
devDefault?: T; // Default value in non-production environments
choices?: readonly T[]; // Restrict to specific values
desc?: string; // Description (for documentation)
example?: string; // Example value (for documentation)
docs?: string; // Link to docs (for documentation)
}Examples
const env = cleanEnv(process.env, {
// Required — will throw if missing
SECRET_KEY: str(),
// With default
PORT: num({ default: 8080 }),
// Dev-only default (ignored in production)
DATABASE_URL: url({ devDefault: 'http://localhost:5432/devdb' }),
// Restricted choices
LOG_LEVEL: str({ choices: ['debug', 'info', 'warn', 'error'] }),
// Documented
REDIS_URL: url({
desc: 'Redis connection string',
example: 'redis://localhost:6379',
docs: 'https://redis.io/docs/connect',
}),
});Custom Validators
Use makeValidator to create your own:
import { makeValidator, cleanEnv } from 'envtrust';
// Custom integer array validator
const intArray = makeValidator<number[]>((input) => {
const nums = input.split(',').map(Number);
if (nums.some(isNaN)) throw new Error('Expected comma-separated integers');
return nums;
}, 'intArray');
// Custom enum validator
const logLevel = makeValidator<'debug' | 'info' | 'warn' | 'error'>((input) => {
const valid = ['debug', 'info', 'warn', 'error'] as const;
if (!valid.includes(input as any)) throw new Error(`Must be one of: ${valid.join(', ')}`);
return input as 'debug' | 'info' | 'warn' | 'error';
}, 'logLevel');
const env = cleanEnv(process.env, {
ALLOWED_PORTS: intArray(), // number[]
LOG_LEVEL: logLevel(), // 'debug' | 'info' | 'warn' | 'error'
});Error Handling
When validation fails, envtrust provides clear, actionable error messages:
Environment validation failed:
- DATABASE_URL: Missing required environment variable: DATABASE_URL
- PORT: Invalid number: "abc"
- API_KEY: Missing required environment variable: API_KEYCustom Reporter
const env = cleanEnv(process.env, specs, {
reporter: ({ errors, env }) => {
// Send to your logging service
logger.fatal('Env validation failed', errors);
process.exit(1);
},
});Environment Helpers
cleanEnv automatically adds these read-only boolean helpers based on NODE_ENV:
env.isProduction // NODE_ENV === 'production'
env.isDev // NODE_ENV === 'development'
env.isTest // NODE_ENV === 'test'Test Utilities
Use testOnly to provide defaults that only apply during testing:
import { cleanEnv, str, testOnly } from 'envtrust';
const env = cleanEnv(process.env, {
API_KEY: str({ default: testOnly('test-key-123') }),
});
// In test: API_KEY = 'test-key-123'
// In prod: API_KEY must be set explicitlyArchitecture
cleanEnv()
│
├── Read process.env
│
├── For each spec:
│ │
│ ├── Value exists?
│ │ ├── Yes → Parse & Validate
│ │ │ ├── Passes choices? → ✅ Add to output
│ │ │ └── Fails? → ❌ EnvError
│ │ │
│ │ └── No → Has default?
│ │ ├── Yes → Use default / devDefault
│ │ └── No → ❌ EnvMissingError
│ │
│ └── Collect errors
│
├── Run reporter (if errors)
└── Freeze & Return ✅Full Example
// env.ts
import { cleanEnv, str, num, bool, url, port, email, json } from 'envtrust';
export const env = cleanEnv(process.env, {
// App
NODE_ENV: str({ choices: ['development', 'production', 'test'] }),
PORT: port({ default: 3000 }),
HOST: str({ default: '0.0.0.0' }),
// Database
DB_URL: url(),
DB_POOL: num({ default: 10 }),
// Auth
JWT_SECRET: str(),
SESSION_TTL: num({ default: 86400 }),
// Email
SMTP_HOST: str({ devDefault: 'localhost' }),
SMTP_PORT: port({ default: 587 }),
ADMIN_EMAIL: email(),
// Feature flags
ENABLE_CACHE: bool({ default: true }),
DEBUG: bool({ default: false }),
// Complex config
CORS_ORIGINS: json({ default: '["http://localhost:3000"]' }),
});
// Use anywhere with full type safety
// env.PORT → number
// env.DB_URL → string
// env.DEBUG → boolean
// env.isProduction → booleanAPI Reference
cleanEnv(environment, specs, options?)
| Parameter | Type | Description |
|-----------|------|-------------|
| environment | Record<string, string \| undefined> | Environment object (usually process.env) |
| specs | Record<string, ValidatorSpec> | Validation schema |
| options.reporter | (opts) => void | Custom error reporter |
Returns: Readonly<{ [key]: ValidatedType }> & { isProduction, isDev, isTest }
makeValidator<T>(parseFn, typeName?)
| Parameter | Type | Description |
|-----------|------|-------------|
| parseFn | (input: string) => T | Parse function that throws on invalid input |
| typeName | string | Optional name for the validator type |
Returns: A validator factory function
testOnly(defaultValue)
Returns the value only when NODE_ENV === 'test', otherwise undefined.
Error Classes
| Class | When |
|-------|------|
| EnvError | Value exists but fails validation |
| EnvMissingError | Required variable is missing |
License
MIT © hemathkumar
