DEV Community

Cover image for Type-safe environment validation
Made Büro
Made Büro

Posted on

Type-safe environment validation

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
Enter fullscreen mode Exit fullscreen mode

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,
  },
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

npm install -g envzen-cli
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)] })
Enter fullscreen mode Exit fullscreen mode

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[]
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Feedback, issues, and contributions are welcome.

Top comments (2)

Collapse
 
theoephraim profile image
Theo Ephraim

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.