TypeScript-first environment variable validation with schema-driven type safety
npm install envproof> TypeScript-first environment variable validation with schema-driven type safety




EnvProof validates environment variables at application startup and fails fast with human-readable errors. No more runtime crashes from missing or invalid configuration.
- ๐ 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
| 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 | โ
| โ ๏ธ | โ ๏ธ | โ
|
``bash`
npm install envproofor
pnpm add envproofor
bun add envproof
`typescript
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
`
`typescript`
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
`typescript`
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
Accepts: true, false, 1, 0, yes, no, on, off (case-insensitive)
`typescript`
e.boolean().optional().default(false);
`typescript`
e.enum(["development", "staging", "production"] as const).default(
"development"
);
> Note: Use as const for proper type inference.
Returns a native URL object for easy manipulation.
`typescript`
e.url()
.protocols(["http", "https"]) // Restrict protocols
.http() // Shorthand for http/https
.withPath() // Require a path
.host("api.example.com"); // Require specific host
Parse and validate JSON configuration.
`typescript`
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");
Parse comma-separated values into arrays.
`typescript`
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
Parse human-readable duration strings into milliseconds.
`typescript`
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).
Validate file system paths.
`typescript`
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
Validate IP addresses.
`typescript`
e.string().ip(); // IPv4 or IPv6
e.string().ip({ version: "v4" }); // IPv4 only
e.string().ip({ version: "v6" }); // IPv6 only
Check out the /examples folder for complete working examples:
- basic.ts - Simple usage
- express.ts - Express.js integration
- nextjs.ts - Next.js configuration
- docker.ts - Docker/container environments
- monorepo.ts - Monorepo shared configuration
`typescript
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",
};
}
},
});
`
Use onError: "return" when you want createEnv to return a ValidationResult
instead of throwing:
`typescript
const result = createEnv(schema, { onError: "return" });
if (!result.success) {
console.error(result.errors);
} else {
console.log(result.data.PORT);
}
`
Chain transformations and custom rules:
`typescript`
e.string()
.transform((s) => s.trim()) // Trim whitespace
.transform((s) => s.toLowerCase()) // Lowercase
.custom((val) => val.startsWith("sk_"), "Must start with sk_"); // Custom rule
Compose and reuse schemas with built-in utilities:
`typescript
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()... }
`
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`
`typescript`
const env = createEnv(schema, { reporter: "json" });
`json`
{
"success": false,
"errorCount": 3,
"errors": [
{
"variable": "DATABASE_URL",
"reason": "missing",
"message": "Required variable is not set",
"expected": "URL",
"isSecret": false
}
]
}
`typescript`
const env = createEnv(schema, { reporter: "minimal" });
``
โ Environment validation failed (3 errors): DATABASE_URL, API_KEY, PORT
`typescript
import { generateExample } from "envproof";
const content = generateExample(schema);
console.log(content);
`
`bash`
npx envproof generate
npx envproof generate --output .env.template
npx envproof generate --force # Overwrite existing
`bash============================================================
Environment Configuration
Generated by envproof
============================================================
CLI Commands
`bash
Validate environment against schema
npx envproof check
npx envproof check --schema ./config/env.ts
npx envproof check --reporter json
npx envproof check --strictGenerate .env.example
npx envproof generate
npx envproof generate --output .env.example
npx envproof generate --forceScaffold starter files
npx envproof init
npx envproof init --schema ./config/env.ts --output .env.example --force
`Best Practices
$3
Create a dedicated file for your env schema:
`typescript
// src/env.ts
import { createEnv, e } from "envproof";export const env = createEnv({
// ... your schema
});
`Import it everywhere:
`typescript
import { env } from "./env";
console.log(env.DATABASE_URL);
`$3
After setting up EnvProof, use only the typed
env object:`typescript
// โ Bad
const port = process.env.PORT;// โ
Good
const port = env.PORT;
`$3
Always mark secrets to prevent accidental logging:
`typescript
API_KEY: e.string().secret(),
DATABASE_URL: e.url().secret(),
`$3
Add descriptions for .env.example generation:
`typescript
PORT: e.number()
.port()
.default(3000)
.description('HTTP server port')
.example('8080'),
`Framework Examples
$3
`typescript
// 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});
});
`$3
`typescript
// 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(),
});
`$3
`typescript
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:
`typescript
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'
`$3
`typescript
import type { InferEnv } from "envproof";const schema = {
PORT: e.number(),
};
type Env = InferEnv;
// { readonly PORT: number }
`Dotenv Utilities
EnvProof exports standalone dotenv utilities for advanced use cases:
`typescript
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 =
;
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
$3
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")$3
Validate without throwing - returns result object.
`typescript
const result = validateEnv(schema);
if (result.success) {
console.log(result.data);
} else {
console.error(result.errors);
}
`$3
Generate .env.example content as string.
$3
Write .env.example file to disk.
$3
-
e.string() - String values
- e.number() - Numeric values
- e.boolean() - Boolean values
- e.enum([...]) - Enumerated values
- e.url() - URL values
- e.json - JSON values
- e.array(itemSchema) - Array values (comma-separated)
- e.duration() - Duration values (e.g., "1h", "30m")
- e.path() - File system paths$3
-
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$3
-
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 keysMIT ยฉ EnvProof Contributors