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:
npm install envzen-core
Then define your schema:
// 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:
// 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.exampleoutput -
sensitive— redacted in logs, errors, andJSON.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:
npm install -g envzen-cli
Available commands:
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:
envzen init --ci
envzen check --ci
Framework adapters
EnvZen works beyond plain Node.js apps with adapters for several common setups:
// 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:
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.examplein 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
- npm: envzen-core · envzen-cli
- License: MIT
Feedback, issues, and contributions are welcome.
Top comments (2)
You might like varlock.dev (also free and open source). Definitely some similar thinking - but built in a way to be usable for any language. Also lets you set values, both directly and by pulling from external sources via a plugin system.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.