SkillAgentSearch skills...

Znv

Type-safe environment parsing and validation for Node.js with Zod schemas

Install / Use

/learn @lostfictions/Znv
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

znv

<p align="center"> <img src="logo.svg" height="90" alt="znv logo"> </p> <p align="center"> <a href="https://www.npmjs.com/package/znv"> <img src="https://img.shields.io/npm/v/znv.svg?logo=npm" alt="NPM version" /> </a> </p>

Parse your environment with Zod.

Pass in a schema and your process.env. Get back a validated, type-safe, read-only environment object that you can export for use in your app. You can optionally provide defaults (which can be matched against NODE_ENV values like production or development), as well as help strings that will be included in the error thrown when an env var is missing.

Features

  • No dependencies
  • Fully type-safe
  • Compatible with serverless environments (import znv/compat instead of znv)

Status

Unstable: znv has not yet hit v1.0.0, and per semver there may be breaking changes in minor versions before the v1.0.0 release. Any (known) breaking changes will be documented in release notes. znv is used in production in several services at the primary author's workplace. Feedback and suggestions about final API design are welcome.

Contents

Quickstart

npm i znv zod
# or
pnpm add znv zod
# or
yarn add znv zod

Create a file named something like env.ts:

import { parseEnv } from "znv";
import { z } from "zod";

export const { NICKNAME, LLAMA_COUNT, COLOR, SHINY } = parseEnv(process.env, {
  NICKNAME: z.string().min(1),
  LLAMA_COUNT: z.number().int().positive(),
  COLOR: z.enum(["red", "blue"]),
  SHINY: z.boolean().default(true),
});

console.log([NICKNAME, LLAMA_COUNT, COLOR, SHINY].join(", "));

Let's run this with ts-node:

$ LLAMA_COUNT=huge COLOR=cyan ts-node env.ts
<img src="example.png" width="658" alt="A screenshot showing error output, with parsing errors aggregated and grouped by env var.">

Oops! Let's fix those issues:

$ LLAMA_COUNT=24 COLOR=red NICKNAME=coolguy ts-node env.ts

Now we see the expected output:

coolguy, 24, red, true

Since parseEnv didn't throw, our exported values are guaranteed to be defined. Their TypeScript types will be inferred based on the schemas we used — COLOR will be even be typed to the union of literal strings 'red' | 'blue' rather than just string.


A more elaborate example:

// znv re-exports zod as 'z' to save a few keystrokes.
import { parseEnv, z, port } from "znv";

export const { API_SERVER, HOST, PORT, EDITORS, POST_LIMIT, AUTH_SERVER } =
  parseEnv(process.env, {
    // you can provide defaults with `.default()`. these will be validated
    // against the schema.
    API_SERVER: z.string().url().default("https://api.llamafy.biz"),

    // specs can also be more detailed.
    HOST: {
      schema: z.string().min(1),

      // the description is handy as in-code documentation, but is also printed
      // to the console if validation for this env var fails.
      description: "The hostname for this service.",

      // instead of specifying defaults as part of the zod schema, you can pass
      // them in the `defaults` object. a default will be matched based on the
      // value of `NODE_ENV`.
      defaults: {
        production: "my-cool-llama.website",
        test: "cool-llama-staging.cloud-provider.zone",

        // "_" is a special token that can be used in `defaults`. its value will
        // be used if `NODE_ENV` doesn't match any other provided key.
        _: "localhost",
      },
    },

    // znv provides helpers for a few very common environment var types not
    // covered by zod. these can have further refinements chained to them:
    PORT: port().default(8080),

    // using a zod `array()` or `object()` as a spec will make znv attempt to
    // `JSON.parse` the env var if it's present.
    EDITORS: z.array(z.string().min(1)),

    // optional values are also supported and provide a way to benefit from the
    // validation and static typing provided by zod even if you don't want to
    // error out on a missing value.
    POST_LIMIT: z.number().optional(),

    // use all of the expressiveness of zod, including enums and post-processing.
    AUTH_SERVER: z
      .enum(["prod", "staging"])
      .optional()
      .transform((prefix) =>
        prefix ? `http://auth-${prefix}.cool-llama.app` : "http://localhost:91",
      ),
  });

If any env var fails validation, parseEnv() will throw. All failing specs will be aggregated in the error message, with each showing the received value, the reason for the failure, and a hint about the var's purpose (if description was provided in the spec).

Motivation

Environment variables are one way to pass runtime configuration into your application. As promoted by the Twelve-Factor App methodology, this helps keep config (which can vary by deployment) cleanly separated from code, encouraging maintainable practices and better security hygiene. But passing in configuration via env vars can often turn into an ad-hoc affair, with access and validation scattered across your codebase. At worst, a misconfigured environment will launch and run without apparent error, with issues only making themselves apparent later when a certain code path is hit. A good way to avoid this is to declare and validate environment variables in one place and export the validated result, so that other parts of your code can make their dependencies on these vars explicit.

Env vars represent one of the boundaries of your application, just like file I/O or a server request. In TypeScript, as in many other typed languages, these boundaries present a challenge to maintaining a well-typed app. Zod does an excellent job at parsing and validating poorly-typed data at boundaries into clean, well-typed values. znv facilitates its use for environment validation.

What does znv actually do?

znv is a small module that works hand-in-hand with Zod. Since env vars, when defined, are always strings, Zod schemas like z.number() will fail to parse them out-of-the-box. Zod allows you to use a preprocess schema to handle coercions, but peppering your schemas with preprocessors to this end is verbose, error-prone, and clunky. znv wraps each of the Zod schemas you pass to parseEnv in a preprocessor that tries to coerce a string to a type the schema expects.

These preprocessors don't do any validation of their own — in fact, they try to do as little work as possible and defer to your schema to handle the validation. In practice, this should be pretty much transparent to you, but you can check out the coercion rules if you'd like more info.

Since v3.20, Zod provides z.coerce for primitive coercion, but this is often too naive to be useful. For example, z.coerce.boolean() will parse "false" into true, since the string "false" is truthy in JavaScript. znv will coerce "false" into false, which is probably what you expect.

znv also makes it easy to define defaults for env vars based on your environment. Zod allows you to add a default value for a schema, but making a given default vary by environment or only act as a fallback in certain environments is not straightforward.

Usage

parseEnv(environment, schemas, reporterOrFormatters?)

Parse the given environment using the given schemas. Returns a read-only object that maps the keys of the schemas object to their respective parsed values.

Throws if any schema fails to parse its respective env var. The error aggregates all parsing failures for the schemas.

Optionally, you can pass a custom error reporter as the third parameter to parseEnv to customize how errors are displayed. The reporter is a function that receives error details and returns a string. Alternately, you can pass an object of token formatters as the third parameter to parseEnv; this can be useful if you want to retain the default error reporting format but want to customize some aspects of it (for example, by redacting secrets).

environment: Record<string, string | undefined>

You usually want to pass in process.env as the first argument.

It is not recommended to use znv for general-purpose schema validation — just use Zod (with preprocessors to handle coercion, if necessary).

schemas: Record<string, ZodType | DetailedSpec>

Maps env var names to validators. You can either use a Zod schema directly, or pass a DetailedSpec object that has the following fields:

  • schema: ZodType

    The Zod validator schema.

  • description?: string

    Optional help text that will be displayed when this env var is missing or fails to validate.

  • defaults?: Record<string, SchemaInput | undefined>

    An object that maps from NODE_ENV values to values that will be passed as input to the schema if this var isn't present in the environment. For example:

    const schemas = {
      FRUIT: {
        schema: z.string().min(1),
        defaults: {
          production: "orange",
          development: "banana",
        },
      },
    };
    
    // FRUIT wll have value "banana".
    const { FRUIT } = parseEnv({ NODE_ENV: "development" }, schemas);
    
    // FRUIT wll have value "orange".
    const { FRUIT } = parseEnv({ NODE_ENV: "production" }, schemas);
    
    // FRUIT wll have value "fig".
    const { FRUIT } = parseEnv({ NODE_ENV: "production", FRUIT: "f
    
View on GitHub
GitHub Stars386
CategoryDevelopment
Updated10d ago
Forks10

Languages

TypeScript

Security Score

100/100

Audited on Mar 17, 2026

No findings