Environment variables are one of those things that look simple until they start breaking production.
A missing `DATABASE_URL`, an invalid `PORT`, a typo in `NODE_ENV`, or a leaked secret in logs — all of these come from the same place: `process.env` is just untrusted input.
That's why I made **EnvZen**: an Open Source toolkit for validating environment variables in TypeScript / Node.js. It validates `process.env` against a schema at startup, returns typed config, redacts sensitive values, and includes a CLI for scaffolding, checking, and syncing `.env` files.
## The problem with process.env
In most Node.js apps, environment variables are used everywhere but validated nowhere.
That usually leads to a few common problems:
- Values are missing, but you only find out after deploy
- Everything is a string, so type coercion gets repeated across the codebase
- Config rules live in people's heads instead of code
- Secrets accidentally end up in logs or JSON output
The core idea behind EnvZen is simple: treat env variables like runtime input and validate them at boot time. EnvZen throws a structured `EnvValidationError` when required values are missing, invalid, or the wrong type. It also infers TypeScript types directly from the schema.
## Quick start
Install the core package:
```bash
npm install envzen-core
```
Then define your schema:
```typescript
// env.ts
import { createEnv } from 'envzen-core'
export const env = createEnv({
NODE_ENV: {
type: 'enum',
values: ['development', 'production', 'test'],
default: 'development',
},
PORT: {
type: 'port',
default: 3000,
},
DATABASE_URL: {
type: 'url',
required: true,
sensitive: true,
},
API_KEY: {
type: 'string',
required: true,
sensitive: true,
},
})
```
And use it in your app:
```typescript
// index.ts
import 'dotenv/config'
import { env } from './env.js'
console.log(env.PORT) // number
console.log(env.NODE_ENV) // 'development' | 'production' | 'test'
console.log(JSON.stringify(env)) // sensitive fields are redacted
```
EnvZen does not load `.env` files for you — if you use dotenv, call it before `createEnv()`.
## What EnvZen gives you
The core package is built around a schema-driven API.
Supported field types: `string`, `number`, `boolean`, `port`, `url`, `email`, `enum`.
You can also mark fields as:
- `required` — must be present, no fallback
- `default` — fallback value when variable is absent
- `description` — used in `.env.example` output
- `sensitive` — redacted in logs, errors, and `JSON.stringify()`
- `validate` — additional Zod-based validation on top of the base type
That means your environment config becomes a real contract instead of a pile of implicit assumptions.
## Safer logging by default
One thing I cared about from the start was avoiding secret leaks.
If a field is marked as `sensitive`, EnvZen automatically redacts it in `toJSON()` and `JSON.stringify()`. Sensitive values are also redacted from validation error messages.
That makes it much safer to inspect config or print error output without accidentally exposing credentials.
## CLI for real workflows
Besides the runtime library, there's also `envzen-cli`:
```bash
npm install -g envzen-cli
```
Available commands:
```bash
envzen init # scaffold env.ts config
envzen sync --schema ./env.ts # generate .env.example from schema
envzen check --schema ./env.ts --env .env # diff .env against schema
```
`init` scaffolds an `env.ts` config file. `sync` generates `.env.example` from your schema — always up to date. `check` diffs your `.env` against the schema and reports missing, extra, or invalid variables.
CI mode is also supported:
```bash
envzen init --ci
envzen check --ci
```
## Framework adapters
EnvZen works beyond plain Node.js apps with adapters for several common setups:
```typescript
// Express / Fastify
import { envGuardMiddleware } from 'envzen-express'
app.use(envGuardMiddleware(schema))
// Vite
import { envGuardPlugin } from 'envzen-vite'
export default { plugins: [envGuardPlugin(schema)] }
// Next.js
import { withEnvGuard } from 'envzen-next'
export default withEnvGuard(nextConfig, schema)
// NestJS
import { EnvGuardModule } from 'envzen-nestjs'
@Module({ imports: [EnvGuardModule.forRoot(schema)] })
```
Each adapter validates at the framework's startup lifecycle. Invalid env = the app doesn't start.
## Error handling
When validation fails, EnvZen throws `EnvValidationError` with both a formatted message and a structured `failures` array:
```typescript
import { createEnv, EnvValidationError } from 'envzen-core'
try {
const env = createEnv(schema)
} catch (err) {
if (err instanceof EnvValidationError) {
console.error(err.message) // human-readable summary
console.error(err.failures) // ValidationFailure[]
}
}
```
## Why I made this
There are already good libraries in this space, but I wanted something with a few specific properties:
- Schema-first API with full TypeScript inference
- Sensitive values redacted by default, not opt-in
- CLI that keeps `.env.example` in sync automatically
- Lightweight adapters for Express, NestJS, Next.js, Vite
The goal was not just "validate env vars" but to make environment configuration feel like part of the application contract.
## Try it out
- GitHub: [github.com/madeburo/envzen](https://github.com/madeburo/envzen)
- npm: [envzen-core](https://npmjs.com/package/envzen-core) · [envzen-cli](https://npmjs.com/package/envzen-cli)
- License: MIT
Feedback, issues, and contributions are welcome.