Skip to content

Zod Adapter

The @frauschert/env-guard-zod package lets you define your environment schema using Zod — giving you access to Zod's full power: z.coerce, z.transform, z.refine, z.enum, z.union, and more.

Installation

bash
npm install @frauschert/env-guard-zod zod

zod >= 3.0.0 and @frauschert/env-guard are peer / transitive dependencies installed automatically.

Quick Start

ts
import { z } from "zod";
import { createZodEnv } from "@frauschert/env-guard-zod";

const env = createZodEnv({
  PORT: z.coerce.number().int().min(1).max(65535),
  HOST: z.string().default("localhost"),
  DATABASE_URL: z.string().url(),
  LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});

// env.PORT       → number
// env.HOST       → string
// env.DATABASE_URL → string
// env.LOG_LEVEL  → "debug" | "info" | "warn" | "error"

API Reference

createZodEnv(schema, options?)

ParameterTypeDescription
schemaZodEnvSchemaObject mapping variable names to Zod types
optionsZodEnvOptions (optional)Global options (env files, prefix, watch, etc.)

Returns InferZodEnv<S>, or WatchableZodEnv<S> when watch: true.

ZodEnvOptions

Identical to EnvOptions from the core package.

PropertyTypeDefaultDescription
envFilesboolean | string[]falseLoad .env files before validation
prefixstringundefinedPrefix prepended when reading env variables
onError(errors: string[]) => voidundefinedCustom error handler, replaces default throw
strictbooleanfalseProxy throws on access to unknown keys
freezebooleanfalseObject.freeze the returned object
watchtrueundefinedReturn a watchable env with refresh()

Options

.env File Loading

ts
const env = createZodEnv(
  { DATABASE_URL: z.string().url() },
  { envFiles: true },
);

Prefix Scoping

ts
// Reads MYAPP_PORT from process.env
const env = createZodEnv({ PORT: z.coerce.number() }, { prefix: "MYAPP_" });

Custom Error Handler

ts
const env = createZodEnv(
  { PORT: z.coerce.number() },
  {
    onError(errors) {
      console.error("Env errors:", errors);
      process.exit(1);
    },
  },
);

Strict Mode

Throws when accessing a key not defined in the schema:

ts
const env = createZodEnv({ HOST: z.string() }, { strict: true });

env.HOST; // ✅
env.UNKNOWN; // ❌ throws

Frozen Output

ts
const env = createZodEnv({ HOST: z.string() }, { freeze: true });
// Object.isFrozen(env) === true

Runtime Refresh

ts
const env = createZodEnv({ API_KEY: z.string() }, { watch: true });

env.on("change", (key, oldValue, newValue) => {
  console.log(`${key} changed: ${oldValue} → ${newValue}`);
});

// Later, when process.env changes:
env.refresh();

freeze and watch are mutually exclusive.

Zod Features

Since each field is a plain Zod type, you get the full Zod feature set:

Coercion

ts
const env = createZodEnv({
  PORT: z.coerce.number(), // "3000" → 3000
  TIMEOUT: z.coerce.number().int(),
});

Defaults

ts
const env = createZodEnv({
  PORT: z.coerce.number().default(3000),
  HOST: z.string().optional().default("localhost"),
});

Enums

ts
const env = createZodEnv({
  NODE_ENV: z.enum(["development", "test", "production"]),
});
// env.NODE_ENV → "development" | "test" | "production"

String Formats

ts
const env = createZodEnv({
  API_URL: z.string().url(),
  CONTACT: z.string().email(),
  TOKEN: z.string().uuid(),
});

Transforms

ts
const env = createZodEnv({
  ALLOWED_ORIGINS: z.string().transform((v) => v.split(",")),
  PORT: z.coerce.number().transform((n) => ({ port: n })),
});
// env.ALLOWED_ORIGINS → string[]

Refinements

ts
const env = createZodEnv({
  PORT: z.coerce
    .number()
    .refine((n) => n >= 1 && n <= 65535, "Port must be between 1 and 65535"),
});

Nested Groups

Plain objects (non-Zod values) are treated as nested groups. The env key is built as GROUP_KEY:

ts
const env = createZodEnv({
  db: {
    HOST: z.string(),
    PORT: z.coerce.number(),
  },
});

// Reads DB_HOST and DB_PORT from process.env
// env.db.HOST → string
// env.db.PORT → number

Comparison with Core createEnv

FeaturecreateEnv (core)createZodEnv (zod adapter)
Zero dependenciesRequires zod
Type inference
Number / boolean parsingBuilt-inz.coerce.number() etc.
Enum / choiceschoices: [...]z.enum([...])
Custom validationvalidate: fnz.refine(fn)
Format presetsformat: "url" etc.z.string().url() etc.
Transformscoerce: fnz.transform(fn) (full pipe)
Nested schemas
Prefix, strict, freeze, watch

Released under the MIT License.