
Environment variables are one of those things that look simple until they start breaking...
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.
In most Node.js apps, environment variables are used everywhere but validated nowhere.
That usually leads to a few common problems:
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.
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().
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 fallbackdefault — fallback value when variable is absentdescription — used in .env.example outputsensitive — redacted in logs, errors, and JSON.stringify()validate — additional Zod-based validation on top of the base typeThat means your environment config becomes a real contract instead of a pile of implicit assumptions.
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.
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
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.
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[]
}
}
There are already good libraries in this space, but I wanted something with a few specific properties:
.env.example in sync automaticallyThe goal was not just "validate env vars" but to make environment configuration feel like part of the application contract.
Feedback, issues, and contributions are welcome.
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...
Workflows from the Neura Market marketplace related to this DeepSeek resource