npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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 envtrust

Zero 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 wins

Example: 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, ontrue) | | 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_KEY

Custom 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 explicitly

Architecture

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 → boolean

API 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