JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 13
  • Score
    100M100P100Q54018F
  • License MIT

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

Package Exports

  • envproof

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