@stonayubi/env-safe
v1.0.0
Published
A TypeScript-first .env validator with schema-based validation, type inference, and rich error reporting
Maintainers
Readme
env-safe
TypeScript-first
.envvalidation with full type inference, zero dependencies, and rich error reporting.
npm install env-safeWhy env-safe?
Runtime crashes from bad environment variables are avoidable. env-safe catches them at startup — before your app takes a single request — and tells you exactly what's wrong.
✖ env-safe: 3 validation errors found
1. [MISSING] DATABASE_URL — PostgreSQL connection string
"DATABASE_URL" is required but was not found in the environment
2. [INVALID FORMAT] SMTP_FROM
"SMTP_FROM" must be a valid email address (got "not-an-email")
3. [OUT OF RANGE] PORT
"PORT" must be <= 65535 (got 99999)
Fix the above environment variables and restart.Quick Start
// src/env.ts
import { createEnv, field } from "env-safe";
export const env = createEnv({
PORT: field.port({ default: 3000 }),
DATABASE_URL: field.url({ description: "PostgreSQL connection string" }),
NODE_ENV: field.enum(["development", "production", "test"] as const),
DEBUG: field.boolean({ default: false }),
SMTP_FROM: field.email({ required: false }),
});
// If any required variable is missing or invalid, the process exits with a
// formatted error message. No crashes later in your code.
// env.PORT → number
// env.DATABASE_URL → string
// env.NODE_ENV → "development" | "production" | "test"
// env.DEBUG → boolean
// env.SMTP_FROM → string | undefinedImport env anywhere in your app — it's a frozen, fully-typed object.
API
createEnv(schema, options?)
Validates at startup. Exits the process with a formatted error if validation fails. Returns a Readonly<> typed object.
const env = createEnv({ ... });validate(schema, options?)
Returns a result object — never throws (unless throwOnError: true).
const result = validate({ PORT: field.port() }, { env: process.env });
if (!result.success) {
result.errors; // ValidationError[]
} else {
result.data; // fully typed
}Options
| Option | Type | Default | Description |
|----------------|-----------------------------------|----------------|--------------------------------------------------|
| env | Record<string, string\|undefined> | process.env | Custom env source (useful for testing) |
| throwOnError | boolean | false | Throw EnvValidationError instead of returning |
| onError | (errors: ValidationError[]) => void | — | Called on validation failure (e.g., for logging) |
Field Types
field.string(opts?)
| Option | Type | Description |
|-------------|--------------------|------------------------------------|
| minLength | number | Minimum string length |
| maxLength | number | Maximum string length |
| pattern | RegExp \| string | Must match this regex |
| trim | boolean | Trim whitespace (default: true) |
field.string({ minLength: 32, pattern: /^[a-zA-Z0-9_]+$/ })field.number(opts?)
| Option | Type | Description |
|-----------|-----------|--------------------------------|
| min | number | Minimum value (inclusive) |
| max | number | Maximum value (inclusive) |
| integer | boolean | Reject floats |
field.number({ min: 1, max: 100, integer: true })field.boolean(opts?)
Accepts: true/false, 1/0, yes/no, on/off (case-insensitive).
field.boolean({ default: false })field.url(opts?)
| Option | Type | Description |
|------------------|------------|--------------------------------------------------|
| protocols | string[] | Allowed protocols (default: ["http:", "https:"]) |
| allowLocalhost | boolean | Allow localhost URLs (default: false) |
field.url({ protocols: ["https:"], allowLocalhost: false })field.email(opts?)
field.email({ required: false })field.port(opts?)
Validates integers between 0–65535.
field.port({ default: 3000 })field.json<T>(opts?)
Parses JSON and optionally types the result.
field.json<{ retries: number; timeout: number }>({ default: { retries: 3, timeout: 5000 } })field.enum(values, opts?)
field.enum(["debug", "info", "warn", "error"] as const, { default: "info" })Common Field Options
All field types accept these base options:
| Option | Type | Default | Description |
|---------------|-----------|---------|--------------------------------------------------------|
| required | boolean | true | Whether the variable must be present |
| default | T | — | Value used when variable is missing |
| description | string | — | Shown in error messages |
| message | string | — | Custom error message override |
Using Without the Helpers (plain objects)
The field.* helpers are optional. You can write the schema as plain objects:
import { validate } from "env-safe";
const result = validate({
PORT: { type: "port", default: 3000 },
DATABASE_URL: {
type: "url",
description: "PostgreSQL connection string",
},
NODE_ENV: {
type: "enum",
values: ["development", "production", "test"] as const,
},
});Type Inference
InferSchema<S> gives you the TypeScript type of the validated result:
import { type InferSchema } from "env-safe";
const schema = {
PORT: field.port({ default: 3000 }),
DEBUG: field.boolean({ required: false }),
} satisfies Schema;
type Env = InferSchema<typeof schema>;
// { PORT: number; DEBUG?: boolean }Testing
Pass a custom env to validate test scenarios without touching process.env:
const result = validate(schema, {
env: {
DATABASE_URL: "https://db.example.com",
PORT: "3000",
},
});Error Handling
import { validate, EnvValidationError } from "env-safe";
try {
const result = validate(schema, { throwOnError: true });
} catch (e) {
if (e instanceof EnvValidationError) {
e.errors; // ValidationError[]
}
}License
MIT
