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

envproof

v1.4.0

Published

TypeScript-first environment variable validation with schema-driven type safety

Downloads

485

Readme

EnvProof

TypeScript-first environment variable validation with schema-driven type safety

npm version CI Bundle Size License: MIT

EnvProof validates environment variables at application startup and fails fast with human-readable errors. No more runtime crashes from missing or invalid configuration.

Features

  • 🔒 Type-safe - Full TypeScript inference, no string | undefined
  • ⚡ Fail-fast - Validation at startup, not runtime
  • 📝 Self-documenting - Schema is the single source of truth
  • 🎯 Explicit coercion - Clear rules for string → number/boolean/etc
  • 🔐 Secret masking - Automatic redaction in error output
  • 📄 .env.example generation - Auto-generate documentation
  • 🧪 Strict mode - Fail on unknown environment variables
  • 🔗 Cross-field validation - Validate constraints across multiple vars
  • 📚 Layered dotenv support - .env + environment-specific overrides
  • 🚀 Zero dependencies - Lightweight and fast
  • 🌐 Framework-agnostic - Works everywhere Node.js runs

Why EnvProof?

| Feature | EnvProof | Zod | t3-env | envalid | | ----------------------- | :------: | :---: | :----: | :-----: | | Zero dependencies | ✅ | ❌ | ❌ | ❌ | | Built-in CLI | ✅ | ❌ | ❌ | ✅ | | .env.example generation | ✅ | ❌ | ❌ | ❌ | | Bundle size | ~5KB | ~60KB | ~65KB | ~15KB | | First-class env focus | ✅ | ❌ | ✅ | ✅ | | Type inference | ✅ | ✅ | ✅ | ⚠️ | | Human-readable errors | ✅ | ⚠️ | ⚠️ | ✅ |

Installation

npm install envproof
# or
pnpm add envproof
# or
bun add envproof

Quick Start

import { createEnv, e } from "envproof";

// Define your schema (single source of truth)
const env = createEnv({
  DATABASE_URL: e.url().description("PostgreSQL connection string"),
  PORT: e.number().port().default(3000),
  NODE_ENV: e.enum(["development", "staging", "production"] as const),
  DEBUG: e.boolean().optional(),
  API_KEY: e.string().secret(),
});

// env is fully typed!
console.log(env.DATABASE_URL); // URL object
console.log(env.PORT); // number (3000 if not set)
console.log(env.NODE_ENV); // 'development' | 'staging' | 'production'
console.log(env.DEBUG); // boolean | undefined
console.log(env.API_KEY); // string

Schema Types

String

e.string()
  .minLength(1) // Minimum length
  .maxLength(255) // Maximum length
  .length(10) // Exact length
  .pattern(/^[A-Z]+$/) // Regex validation
  .email() // Email format
  .uuid() // UUID format
  .nonEmpty() // Must not be empty/whitespace
  .startsWith("sk_") // Must start with prefix
  .endsWith(".json") // Must end with suffix
  .ip() // IP address (IPv4 or IPv6)
  .ip({ version: "v4" }) // IPv4 only
  .secret() // Mask in error output
  .optional() // Allow undefined
  .default("fallback") // Default value
  .description("...") // Documentation
  .example("example_value"); // Example for .env.example

Number

e.number()
  .min(0) // Minimum value
  .max(100) // Maximum value
  .integer() // Must be integer
  .positive() // Must be > 0
  .nonNegative() // Must be >= 0
  .port() // Valid port (1-65535)
  .between(1, 10); // Range shorthand

Boolean

Accepts: true, false, 1, 0, yes, no, on, off (case-insensitive)

e.boolean().optional().default(false);

Enum

e.enum(["development", "staging", "production"] as const).default(
  "development"
);

Note: Use as const for proper type inference.

URL

Returns a native URL object for easy manipulation.

e.url()
  .protocols(["http", "https"]) // Restrict protocols
  .http() // Shorthand for http/https
  .withPath() // Require a path
  .host("api.example.com"); // Require specific host

JSON

Parse and validate JSON configuration.

e.json<{ host: string; port: number }>()
  .object() // Must be an object
  .array() // Must be an array
  .validate((v) => v.port > 0, "Port must be positive");

Array

Parse comma-separated values into arrays.

e.array(e.string()); // "a,b,c" -> ["a", "b", "c"]
e.array(e.number()).separator(";"); // "1;2;3" -> [1, 2, 3]
e.array(e.string().email())
  .minLength(1) // Minimum array length
  .maxLength(10); // Maximum array length

Duration

Parse human-readable duration strings into milliseconds.

e.duration(); // "10m" -> 600000
e.duration().default("24h"); // Default: 86400000 (string supported!)
e.duration().default(5000); // Or use milliseconds directly
e.duration().min("1s").max("1h"); // "30s" -> 30000

Supports: ms, s, m, h, d, w (and long forms like seconds).

Path

Validate file system paths.

e.path()
  .exists() // Must exist on disk
  .isFile() // Must be a file
  .isDirectory() // Must be a directory
  .absolute() // Must be absolute path
  .extension(".json") // specific extension
  .readable() // Must be readable
  .writable(); // Must be writable

IP Address

Validate IP addresses.

e.string().ip(); // IPv4 or IPv6
e.string().ip({ version: "v4" }); // IPv4 only
e.string().ip({ version: "v6" }); // IPv6 only

Advanced Examples

Check out the /examples folder for complete working examples:

Configuration Options

const env = createEnv(schema, {
  // Custom source (default: process.env)
  source: process.env,

  // Prefix filtering
  prefix: "APP_", // Only read APP_* variables
  stripPrefix: true, // APP_PORT -> PORT in output

  // Error handling
  onError: "throw", // 'throw' | 'exit' | 'return'
  exitCode: 1, // Exit code when onError: 'exit'

  // Output format
  reporter: "pretty", // 'pretty' | 'json' | 'minimal'

  // Strict mode
  strict: false, // Fail on unknown vars
  strictIgnore: ["HOME", "PATH"], // Optional allow-list when strict=true

  // Dotenv Loading
  dotenv: true, // Load .env files automatically
  dotenvPath: [".env", ".env.local"], // Custom layered paths
  dotenvExpand: true, // Expand ${VAR} references

  // Multi-Environment
  environment: process.env.NODE_ENV, // Current environment
  requireInProduction: ["API_KEY"], // Make optional vars required in prod
  optionalInDevelopment: ["SENTRY_DSN"], // Make required vars optional in dev

  // Cross-field validation
  crossValidate: (env) => {
    if (env.NODE_ENV === "production" && !env.API_KEY) {
      return {
        variable: "API_KEY",
        message: "API_KEY is required in production",
      };
    }
  },
});

onError: "return" Mode

Use onError: "return" when you want createEnv to return a ValidationResult instead of throwing:

const result = createEnv(schema, { onError: "return" });

if (!result.success) {
  console.error(result.errors);
} else {
  console.log(result.data.PORT);
}

Transforms & Custom Validators

Chain transformations and custom rules:

e.string()
  .transform((s) => s.trim()) // Trim whitespace
  .transform((s) => s.toLowerCase()) // Lowercase
  .custom((val) => val.startsWith("sk_"), "Must start with sk_"); // Custom rule

Schema Composition

Compose and reuse schemas with built-in utilities:

import { mergeSchemas, extendSchema, pickSchema, omitSchema, prefixSchema } from "envproof";

// Merge multiple schemas
const baseSchema = {
  NODE_ENV: e.enum(['dev', 'prod'] as const),
  LOG_LEVEL: e.enum(['debug', 'info'] as const),
};

const serverSchema = {
  PORT: e.number().port(),
  HOST: e.string().default('localhost'),
};

const fullSchema = mergeSchemas(baseSchema, serverSchema);

// Extend a base schema
const extended = extendSchema(baseSchema, {
  API_KEY: e.string().secret(),
});

// Pick specific fields
const dbOnly = pickSchema(fullSchema, ['DATABASE_URL', 'REDIS_URL']);

// Omit specific fields
const withoutPort = omitSchema(fullSchema, ['PORT']);

// Add prefix to all keys
const prefixed = prefixSchema(serverSchema, 'APP_');
// Result: { APP_PORT: e.number().port(), APP_HOST: e.string()... }

Error Output

When validation fails, EnvProof provides clear, actionable error messages:

╭─────────────────────────────────────────────────────────────────╮
│                    ❌ Environment Validation Failed              │
│                         3 errors found                           │
╰─────────────────────────────────────────────────────────────────╯

┌─ MISSING VARIABLES ─────────────────────────────────────────────┐
│                                                                  │
│  DATABASE_URL                                                    │
│    ├─ Status:   Missing (required)                               │
│    ├─ Expected: URL (PostgreSQL connection string)               │
│    └─ Example:  postgresql://user:pass@localhost:5432/db         │
│                                                                  │
│  API_KEY                                                         │
│    ├─ Status:   Missing (required)                               │
│    ├─ Expected: string (secret)                                  │
│    └─ Example:  sk_***                                           │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

┌─ INVALID VALUES ────────────────────────────────────────────────┐
│                                                                  │
│  PORT                                                            │
│    ├─ Status:   Invalid type                                     │
│    ├─ Expected: number (integer, 1-65535)                        │
│    ├─ Received: "not-a-number"                                   │
│    └─ Example:  3000                                             │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

💡 Tip: Run `npx envproof generate` to create a .env.example file

JSON Reporter (for CI)

const env = createEnv(schema, { reporter: "json" });
{
  "success": false,
  "errorCount": 3,
  "errors": [
    {
      "variable": "DATABASE_URL",
      "reason": "missing",
      "message": "Required variable is not set",
      "expected": "URL",
      "isSecret": false
    }
  ]
}

Minimal Reporter (for logs)

const env = createEnv(schema, { reporter: "minimal" });
❌ Environment validation failed (3 errors): DATABASE_URL, API_KEY, PORT

.env.example Generation

Programmatic

import { generateExample } from "envproof";

const content = generateExample(schema);
console.log(content);

CLI

npx envproof generate
npx envproof generate --output .env.template
npx envproof generate --force  # Overwrite existing

Example Output

# ============================================================
# Environment Configuration
# Generated by envproof
# ============================================================

# PostgreSQL connection string
# Required: yes
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb

# Server port
# Required: no (default: 3000)
# PORT=3000

# Application environment
# Required: yes
# Options: development | staging | production
NODE_ENV=development

# Enable debug logging
# Required: no
# DEBUG=true

# External API key
# Required: yes
# ⚠️  This is a secret - do not commit real values
API_KEY=your_secret_here

CLI Commands

# Validate environment against schema
npx envproof check
npx envproof check --schema ./config/env.ts
npx envproof check --reporter json
npx envproof check --strict

# Generate .env.example
npx envproof generate
npx envproof generate --output .env.example
npx envproof generate --force

# Scaffold starter files
npx envproof init
npx envproof init --schema ./config/env.ts --output .env.example --force

Best Practices

1. Single Schema File

Create a dedicated file for your env schema:

// src/env.ts
import { createEnv, e } from "envproof";

export const env = createEnv({
  // ... your schema
});

Import it everywhere:

import { env } from "./env";
console.log(env.DATABASE_URL);

2. Never Use process.env Directly

After setting up EnvProof, use only the typed env object:

// ❌ Bad
const port = process.env.PORT;

// ✅ Good
const port = env.PORT;

3. Mark Sensitive Variables

Always mark secrets to prevent accidental logging:

API_KEY: e.string().secret(),
DATABASE_URL: e.url().secret(),

4. Document Everything

Add descriptions for .env.example generation:

PORT: e.number()
  .port()
  .default(3000)
  .description('HTTP server port')
  .example('8080'),

Framework Examples

Express

// src/env.ts
import { createEnv, e } from "envproof";

export const env = createEnv({
  PORT: e.number().port().default(3000),
  NODE_ENV: e.enum(["development", "production"] as const),
});

// src/index.ts
import express from "express";
import { env } from "./env";

const app = express();
app.listen(env.PORT, () => {
  console.log(`Server running on port ${env.PORT}`);
});

Next.js

// env.config.ts
import { createEnv, e } from "envproof";

export const env = createEnv({
  DATABASE_URL: e.url(),
  NEXTAUTH_SECRET: e.string().secret(),
  NEXTAUTH_URL: e.url().optional(),
});

Serverless (AWS Lambda)

import { createEnv, e } from "envproof";

const env = createEnv({
  TABLE_NAME: e.string(),
  AWS_REGION: e.string().default("us-east-1"),
});

export const handler = async (event) => {
  // env is validated before handler runs
  const tableName = env.TABLE_NAME;
};

TypeScript

EnvProof provides full type inference:

const env = createEnv({
  PORT: e.number().default(3000),
  DEBUG: e.boolean().optional(),
  NODE_ENV: e.enum(["dev", "prod"] as const),
});

// Types are inferred:
// env.PORT    -> number
// env.DEBUG   -> boolean | undefined
// env.NODE_ENV -> 'dev' | 'prod'

Type Extraction

import type { InferEnv } from "envproof";

const schema = {
  PORT: e.number(),
};

type Env = InferEnv<typeof schema>;
// { readonly PORT: number }

Dotenv Utilities

EnvProof exports standalone dotenv utilities for advanced use cases:

import {
  loadDotenv,
  loadDotenvFiles,
  expandDotenvVars,
  parseDotenv,
} from "envproof";

// Load .env file (similar to dotenv)
loadDotenv(); // Loads .env by default
loadDotenv(".env.local"); // Custom path

// Load multiple .env files with priority
loadDotenvFiles(".env", ".env.local"); // .env.local takes precedence

// Expand variable references
expandDotenvVars({
  HOST: "localhost",
  API_URL: "https://${HOST}/api",
});

// Parse .env file content
const envContent = `
DATABASE_URL=postgresql://localhost:5432/db
PORT=3000
`;
const parsed = parseDotenv(envContent);
console.log(parsed); // { DATABASE_URL: '...', PORT: '3000' }

These utilities can be used independently of createEnv() if you need custom dotenv loading logic.

API Reference

createEnv(schema, options?)

Main function to validate and create typed env object.

  • schema: Record of variable names to schema definitions
  • options: Configuration options (optional)
  • returns: Frozen env object by default, or ValidationResult with onError: "return"
  • throws: EnvValidationError if validation fails (unless onError: "exit" or "return")

validateEnv(schema, options?)

Validate without throwing - returns result object.

const result = validateEnv(schema);
if (result.success) {
  console.log(result.data);
} else {
  console.error(result.errors);
}

generateExample(schema, options?)

Generate .env.example content as string.

writeExampleFile(schema, options?)

Write .env.example file to disk.

e.* Schema Builders

  • e.string() - String values
  • e.number() - Numeric values
  • e.boolean() - Boolean values
  • e.enum([...]) - Enumerated values
  • e.url() - URL values
  • e.json<T>() - JSON values
  • e.array(itemSchema) - Array values (comma-separated)
  • e.duration() - Duration values (e.g., "1h", "30m")
  • e.path() - File system paths

Dotenv Utilities

  • loadDotenv(path?) - Parse one .env file and return key-value pairs
  • loadDotenvFiles(...paths) - Parse and merge multiple .env files
  • expandDotenvVars(vars, context?) - Expand ${VAR} references in dotenv values
  • parseDotenv(content) - Parse .env file content to object

Schema Composition Utilities

  • mergeSchemas(...schemas) - Merge multiple schemas into one
  • extendSchema(base, extension) - Extend a base schema with additional fields
  • pickSchema(schema, keys) - Create a schema with only specified keys
  • omitSchema(schema, keys) - Create a schema without specified keys
  • prefixSchema(schema, prefix) - Add a prefix to all schema keys

License

MIT © EnvProof Contributors