envforge-ts
v1.1.1
Published
Type-safe environment variable and config validation for TypeScript using Zod.
Maintainers
Readme
🔥 envforge-ts
Type-safe environment variable and config validation for TypeScript and Node.js
Built on Zod. Zero runtime dependencies besides Zod. Fully typed. Production-ready.
npm install envforge-ts zod📖 Documentation · 🚀 Features · 💡 Examples · 🔌 Framework Integrations · 🪝 Git Hooks
🚀 Features
- ✅ Type-safe parsing – Full TypeScript inference on parsed outputs
- ✅ Elegant DSL – Intuitive schema builders for env/config validation
- ✅ Safe by default – Rich diagnostics with grouped errors
- ✅ Secret protection – Automatic redaction of sensitive values in logs
- ✅ Colorized output – Beautiful, readable error formatting with chalk
- ✅ Flexible parsing –
safeParse,parseOrThrow, and formatted error output - ✅ Zero config – Works with
process.envout of the box - ✅ Extensible – Built-in source adapters; easy to add custom ones
- ✅ Strict mode – Optional strict validation; permissive by default for env vars
- ✅ Production-ready – Dual ESM/CJS, declaration files, tree-shaking
- ✅ Fully tested – Unit, integration, and type inference tests
- ✅ Build-time validation – Vite, Webpack, and Next.js plugins validate env before your app bundles
- ✅ NestJS module – Drop-in
EnvForgeModulefor startup validation in NestJS apps - ✅ Git hooks – Block
.envfile commits and validate env on push — framework-agnostic
📖 Quick Start
1. Define your schema
import { defineEnv, enumValue, number, secret, string } from 'envforge-ts';
const schema = defineEnv({
NODE_ENV: enumValue(['development', 'test', 'production']),
PORT: number({ min: 1, max: 65535 }).default(3000),
DATABASE_URL: string().min(1),
API_KEY: secret(string().min(10)),
});2. Parse and use
// Throw on validation error
const env = parseEnv(schema);
console.log(env.PORT); // number | 3000
// Or handle errors gracefully
const result = safeParse(schema, process.env);
if (!result.success) {
console.error(formatErrors(result.error));
process.exit(1);
}
const config = result.data;3. See beautifully formatted errors
❌ Validation failed for env with 2 issue(s).
Error code: VALIDATION_FAILED
Scope: env
📍 API_KEY
⚠️ [too_small] String must contain at least 10 character(s)
input: [REDACTED]
📍 PORT
⚠️ [invalid_type] Expected number, received string
input: abcFeatures:
- ✨ Auto-colored output in TTY environments (disable with
color: false) - 🚫 Secrets automatically redacted
- 📍 Grouped by field with visual hierarchy
- 🎨 Emoji indicators for clarity
💡 Examples
Parse environment variables
import { defineEnv, number, parseEnv, string } from 'envforge-ts';
const env = defineEnv({
DATABASE_HOST: string().default('localhost'),
DATABASE_PORT: number({ min: 1, max: 65535 }).default(5432),
DEBUG: boolean().optional(),
});
const config = parseEnv(env);
// → { DATABASE_HOST: 'localhost', DATABASE_PORT: 5432 }Validate config objects
import { defineConfig, parseConfig } from 'envforge-ts';
import { z } from 'zod';
const schema = defineConfig(
{
serviceName: string().min(1),
retries: z.number().int().min(0).max(5),
timeout: number().default(5000),
},
{ unknownKeys: 'strict' }
);
const config = parseConfig(schema, {
serviceName: 'my-api',
retries: 3,
timeout: 10000,
});Secret protection
import { defineEnv, secret, string, safeParse, formatErrors } from 'envforge-ts';
const schema = defineEnv({
API_KEY: secret(string().min(32)),
DATABASE_PASSWORD: secret(string().min(8)),
SENTRY_DSN: secret(string().url()),
});
const result = safeParse(schema, process.env);
if (!result.success) {
// Secret values are automatically redacted in error output
console.error(formatErrors(result.error));
// API_KEY input will show as [REDACTED], not the actual value
}Custom sources
import { defineEnv, parseEnv, processEnvSource, fromRecord } from 'envforge-ts';
// Use specific object instead of process.env
const customEnv = fromRecord({
NODE_ENV: 'production',
PORT: '8080',
});
const schema = defineEnv({
NODE_ENV: enumValue(['development', 'production']),
PORT: number(),
});
const config = parseEnv(schema, customEnv);📚 API Reference
Core Functions
defineEnv(schema, options?)
Define an environment variable schema with passthrough mode by default.
const schema = defineEnv(
{
NODE_ENV: enumValue(['development', 'production']),
PORT: number({ min: 1, max: 65535 }),
},
{ unknownKeys: 'passthrough' } // optional; this is default
);defineConfig(schema, options?)
Define a config schema with strict mode by default.
const schema = defineConfig(
{
appName: string().min(1),
retries: number().default(1),
},
{ unknownKeys: 'strict' } // optional; this is default
);parseEnv(definition, source?, options?)
Parse environment variables and throw on error.
const env = parseEnv(schema);
const env = parseEnv(schema, processEnvSource(process.env));parseConfig(definition, config, options?)
Parse config object and throw on error.
const config = parseConfig(schema, configObject);safeParse(definition, input, options?)
Parse with error handling.
const result = safeParse(schema, process.env);
if (result.success) {
const data = result.data;
} else {
const diagnostics = result.error;
}formatErrors(diagnostics, options?)
Format validation errors into human-readable, colored output.
const message = formatErrors(diagnostics, {
includeInput: true, // Show input values (default: true)
color: true // Use colors in output (default: true in TTY)
});
console.error(message);Output includes:
- 🎨 Color-coded messages (red for errors, yellow for codes, cyan for scope)
- 📍 Visual path indicators
- ⚠️ Issue markers with error codes
- 🚫 Automatically redacted secrets
Schema Builders
| Builder | Type | Options | Example |
|---------|------|---------|---------|
| string() | string | trim, minLength, maxLength | string({ minLength: 1 }) |
| number() | number | int, min, max | number({ min: 0, max: 100 }) |
| boolean() | boolean | — | boolean() |
| enumValue() | T (union) | — | enumValue(['dev', 'prod']) |
| url() | URL | — | url() |
| json() | T | optional schema | json(z.object({ x: z.number() })) |
| array() | T[] | delimiter | array(string(), { delimiter: ',' }) |
| secret() | marks path | — | secret(string().min(10)) |
Source Adapters
processEnvSource(env?)
Create a source from process.env or custom env object.
const source = processEnvSource(); // uses process.env
const source = processEnvSource({ NODE_ENV: 'test' });fromRecord(record)
Create a source from a plain object.
const source = fromRecord({ PORT: '3000', DEBUG: 'true' });🎯 Why envforge-ts?
vs. Dotenv + manual validation
// ❌ Before: Manual, error-prone, no types
const port = parseInt(process.env.PORT, 10);
if (!port || port < 1 || port > 65535) {
throw new Error('Invalid PORT');
}
// ✅ After: Declarative, type-safe, automatic
const { port } = parseEnv(defineEnv({
PORT: number({ min: 1, max: 65535 }).default(3000),
}));vs. Zod directly
// ✅ envforge-ts is Zod-powered
// You get all of Zod's power + env-specific shortcuts
const schema = defineEnv({
API_KEY: secret(string().min(32)), // auto-redacted secrets
PORT: number({ min: 1, max: 65535 }), // coerces "3000" → 3000
FEATURES: array(string()), // coerces "a,b,c" → ['a', 'b', 'c']
DEBUG: boolean(), // coerces "true" → true
});
// Plus automatic grouping, formatting, and diagnostics🛠️ Installation
npm
npm install envforge-ts zodYarn
yarn add envforge-ts zodpnpm
pnpm add envforge-ts zodBun
bun add envforge-ts zodModes & Behavior
unknownKeys
Controls how unknown keys are handled:
// passthrough (default for env)
const env = defineEnv({ PORT: number() });
parseEnv(env, { PORT: '3000', UNKNOWN: 'value' }); // ✅ passes
// strict (default for config)
const config = defineConfig({ name: string() });
parseConfig(config, { name: 'app', unknown: 'value' }); // ❌ errorSecret redaction
Automatically redact sensitive values in diagnostics:
const schema = defineEnv({
API_KEY: secret(string().min(10)),
});
const result = safeParse(schema, { API_KEY: 'short' });
if (!result.success) {
formatErrors(result.error);
// Input shows [REDACTED], not the actual value
}� Framework Integrations
envforge-ts ships dedicated integrations for build-time and startup validation. Each integration is a separate export so you only pay for what you use.
Validation Modes
| Mode | How it works | When it runs |
|------|-------------|--------------|
| Runtime | Call parseEnv / parseConfig in your app | When your app starts |
| Build time | Add the Vite or Webpack plugin | Before bundling |
| Startup (NestJS) | Import EnvForgeModule | During NestFactory.create |
| Git hooks | Run npx envforge install-hooks | On git commit / git push |
You can combine any of these — e.g. build-time plugin and git hooks for layered enforcement.
Vite (+ SvelteKit, Astro, Remix/Vite, Nuxt)
# No extra install — uses envforge-ts/vite sub-path// vite.config.ts
import { defineConfig } from 'vite';
import { envForgeVitePlugin } from 'envforge-ts/vite';
import { envSchema } from './src/env'; // your defineEnv(...) export
export default defineConfig({
plugins: [
envForgeVitePlugin({
schema: envSchema,
// source: '.env', // read from a .env file instead of process.env
enforce: 'error', // 'error' (default) | 'warn'
}),
],
});The plugin runs at buildStart and configureServer, so it catches problems both during vite build and vite dev.
Next.js (Webpack)
// next.config.ts
import type { NextConfig } from 'next';
import { EnvForgeWebpackPlugin } from 'envforge-ts/webpack';
import { envSchema } from './src/env';
const nextConfig: NextConfig = {
webpack(config) {
config.plugins.push(
new EnvForgeWebpackPlugin({
schema: envSchema,
// source: '.env.local', // optional: read from a specific .env file
enforce: 'error',
}),
);
return config;
},
};
export default nextConfig;Works with Next.js, Angular, and any other Webpack-based setup.
NestJS
// app.module.ts
import { Module } from '@nestjs/common';
import { EnvForgeModule } from 'envforge-ts/nest';
import { envSchema } from './env';
@Module({
imports: [
EnvForgeModule.forRoot({
schema: envSchema,
enforce: 'error', // fail bootstrap if env is invalid
}),
],
})
export class AppModule {}EnvForgeModule is a global module. It runs validation in onModuleInit, so your app never fully bootstraps with bad env.
Integration Options
All integrations accept the same BaseIntegrationOptions:
type BaseIntegrationOptions<TSchema> = {
/** A Definition returned by defineEnv() or defineConfig() */
schema: Definition<TSchema>;
/**
* Where to read env values at build time.
* 'process' (default) — uses process.env (works when dotenv is pre-loaded)
* './path/to/.env' — reads and parses a specific .env file directly
*/
source?: 'process' | string;
/**
* What to do on validation failure.
* 'error' (default) — halt the build / throw
* 'warn' — print and continue
*/
enforce?: 'error' | 'warn';
};🪝 Git Hooks
Git hooks are the most framework-agnostic enforcement layer. They work in any project regardless of bundler or framework.
Install hooks
npx envforge install-hooksThis writes two hooks to .git/hooks/:
| Hook | What it does |
|------|-------------|
| pre-commit | Scans staged files for .env patterns and blocks the commit if a secrets file is about to be pushed |
| pre-push | Runs npm run envforge:validate (if defined) or npx envforge validate to catch env issues before they reach your remote |
.env leak protection (pre-commit)
The pre-commit hook automatically detects and blocks commits that include files like:
.env.env.local,.env.production,.env.staging, etc.
Safe files (.env.example, .env.sample, .env.template) are allowed through.
⚠️ [envforge-ts] Potential secret leak detected!
Staged file that looks like an env file: .env.local
If this file contains secrets, remove it from staging:
git restore --staged .env.local
Add it to .gitignore to prevent future accidents:
echo '.env.local' >> .gitignore
[envforge-ts] Commit blocked. Remove the env file(s) above before committing.
To bypass (not recommended): git commit --no-verifySchema validation on push (pre-push)
Add a validate script to your package.json so the pre-push hook knows what to run:
{
"scripts": {
"envforge:validate": "envforge validate --schema ./src/env.ts"
}
}Or run the CLI directly:
npx envforge validate --schema ./src/env.ts
npx envforge validate --schema ./src/env.ts --env .env.production # validate against a specific .env file
npx envforge validate --schema ./src/env.ts --warn # warn instead of errorForce-reinstall or install a single hook
npx envforge install-hooks --force # overwrite existing hooks
npx envforge install-hooks --hook pre-commit # only install pre-commit
npx envforge install-hooks --hook pre-push # only install pre-pushProgrammatic hook installation
import { installHooks } from 'envforge-ts/hooks';
const result = installHooks({ force: true });
console.log(result.installed); // ['pre-commit', 'pre-push']🗺️ Supported Frameworks & Environments
| Framework / Tool | Integration | Import path |
|-----------------|-------------|-------------|
| Vite | Build-time plugin | envforge-ts/vite |
| SvelteKit | Build-time plugin (via Vite) | envforge-ts/vite |
| Astro | Build-time plugin (via Vite) | envforge-ts/vite |
| Remix (Vite mode) | Build-time plugin (via Vite) | envforge-ts/vite |
| Nuxt | Build-time plugin (via Vite) | envforge-ts/vite |
| Next.js | Build-time Webpack plugin | envforge-ts/webpack |
| Angular | Build-time Webpack plugin | envforge-ts/webpack |
| NestJS | Bootstrap module | envforge-ts/nest |
| Express / Fastify / Hapi | Runtime (parseEnv) | envforge-ts |
| Any Node.js app | Runtime (parseEnv) + git hooks | envforge-ts |
| Any git repo | Git hooks (CI/CD-friendly) | npx envforge install-hooks |
🚀 Roadmap
- [x] Build-time Vite plugin
- [x] Build-time Webpack / Next.js plugin
- [x] NestJS module
- [x] Git hooks (pre-commit env leak detection, pre-push validation)
- [x] CLI for hook installation and schema validation
- [ ] CLI: generate
.env.examplefrom a schema - [ ] Config file format support (YAML, TOML, JSON5)
- [ ] Async validators and remote config sources
- [ ] Enhanced diagnostics with suggestions
📄 License
MIT © 2026
Forge your configs with confidence 🔥
