npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yedoma-labs/bylyt-env-guard

v0.3.5

Published

Type-safe, zero-dependency environment variable validation with fail-fast semantics

Downloads

1,414

Readme

@yedoma-labs/bylyt-env-guard

CI npm version npm downloads

Type-safe, zero-dependency environment variable validation for Node.js. Fail fast with clear, collected error messages.

Features

  • 🔒 Type-safe — Full TypeScript inference, result is Readonly<...> and deep-frozen
  • 📦 Zero dependencies — Built-in .env parser, no dotenv needed
  • 💥 Fail-fast — All validation errors collected and thrown at once
  • 🔄 Auto-coercion — Strings to numbers, booleans, dates, JSON, arrays automatically
  • 🎭 Sensitive masking — Mark secrets to hide values in error output
  • 📁 Multi-source.env files + process.env with configurable priority
  • 🏗️ Nested variables — Group related vars (DB__HOST, DB__PORT) into typed objects
  • 🔁 Watch mode — Re-validate automatically when .env files change
  • 📝 Auto-documentation — Generate .env.example and markdown docs from schema

Install

npm install @yedoma-labs/bylyt-env-guard
bun add @yedoma-labs/bylyt-env-guard
pnpm add @yedoma-labs/bylyt-env-guard

Quick Start

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    NODE_ENV: eg.enum(["development", "staging", "production"] as const),
    PORT: eg.port().default(3000),
    DATABASE_URL: eg.url().sensitive(),
    API_KEY: eg.string().minLength(32).sensitive(),
    DEBUG: eg.boolean().default(false),
  },
});

// env is fully typed and immutable:
// {
//   NODE_ENV: "development" | "staging" | "production"
//   PORT: number
//   DATABASE_URL: string
//   API_KEY: string
//   DEBUG: boolean
// }

Real-World Examples

Express / Fastify API Server

A complete example for a typical Node.js API. Create src/env.ts:

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    // Server
    NODE_ENV: eg.enum(["development", "production", "test"] as const),
    PORT: eg.port().default(3000).describe("HTTP port the API server listens on"),
    HOST: eg.string().default("0.0.0.0").describe("Bind address"),

    // Database
    DATABASE_URL: eg.url()
      .protocols("postgres", "postgresql")
      .sensitive()
      .describe("PostgreSQL connection string"),
    DATABASE_POOL_MIN: eg.integer().min(1).max(20).default(2),
    DATABASE_POOL_MAX: eg.integer().min(1).max(100).default(10),

    // Auth
    JWT_SECRET: eg.string().minLength(32).sensitive(),
    JWT_EXPIRES_IN: eg.string().default("7d").describe("e.g. 60, 2d, 10h, 7d"),

    // External APIs
    STRIPE_SECRET_KEY: eg.string().sensitive().requiredIf((raw) => raw.NODE_ENV === "production"),
    STRIPE_WEBHOOK_SECRET: eg.string().sensitive().optional(),

    // Feature flags
    FEATURE_NEW_DASHBOARD: eg.boolean().default(false),
    RATE_LIMIT_RPM: eg.integer().min(10).max(10000).default(100),

    // Logging
    LOG_LEVEL: eg.enum(["debug", "info", "warn", "error"] as const).default("info"),
    LOG_FORMAT: eg.enum(["json", "pretty"] as const).default("json"),
  },
  profiles: {
    development: { LOG_LEVEL: "debug", LOG_FORMAT: "pretty", DATABASE_POOL_MIN: "1" },
    test:        { LOG_LEVEL: "error", DATABASE_POOL_MAX: "2" },
  },
});

// Usage: import { env } from "./env"
// env.PORT         → number
// env.DATABASE_URL → string
// env.NODE_ENV     → "development" | "production" | "test"

This file is the single source of truth for all configuration. Import env anywhere in your app — TypeScript will catch any typos.


Next.js Application

Next.js has server-side and client-side env vars. Use two separate schemas:

// lib/env/server.ts — only imported in server components / API routes
import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const serverEnv = createEnv({
  schema: {
    DATABASE_URL: eg.url().sensitive(),
    NEXTAUTH_SECRET: eg.string().minLength(32).sensitive(),
    NEXTAUTH_URL: eg.url(),
    RESEND_API_KEY: eg.string().sensitive(),
    STRIPE_SECRET_KEY: eg.string().sensitive(),
  },
  sources: [process.env],
});

// lib/env/client.ts — safe for browser bundles
import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const clientEnv = createEnv({
  schema: {
    NEXT_PUBLIC_APP_URL: eg.url(),
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: eg.string(),
    NEXT_PUBLIC_POSTHOG_KEY: eg.string().optional(),
  },
  sources: [process.env],
  prefix: "NEXT_PUBLIC_",  // reads NEXT_PUBLIC_APP_URL as APP_URL etc. — optional
});

Multi-Service Monorepo (Database + Redis + S3)

Group related variables with eg.group():

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    NODE_ENV: eg.enum(["development", "production"] as const),

    // Reads DB__HOST, DB__PORT, DB__NAME, DB__USER, DB__PASSWORD
    db: eg.group({
      HOST: eg.string().default("localhost"),
      PORT: eg.port().default(5432),
      NAME: eg.string().default("myapp"),
      USER: eg.string().default("postgres"),
      PASSWORD: eg.string().sensitive(),
    }),

    // Reads REDIS__HOST, REDIS__PORT, REDIS__PASSWORD
    redis: eg.group({
      HOST: eg.string().default("localhost"),
      PORT: eg.port().default(6379),
      PASSWORD: eg.string().sensitive().optional(),
    }),

    // Reads S3__BUCKET, S3__REGION, S3__ACCESS_KEY_ID, S3__SECRET_ACCESS_KEY
    s3: eg.group({
      BUCKET: eg.string(),
      REGION: eg.string().default("eu-central-1"),
      ACCESS_KEY_ID: eg.string().sensitive(),
      SECRET_ACCESS_KEY: eg.string().sensitive(),
    }),
  },
});

// env.db.HOST     → string
// env.db.PORT     → number
// env.redis.HOST  → string
// env.s3.BUCKET   → string

Your .env file for this schema:

DB__HOST=localhost
DB__PORT=5432
DB__NAME=myapp
DB__USER=postgres
DB__PASSWORD=supersecret

REDIS__HOST=localhost
REDIS__PORT=6379

S3__BUCKET=my-bucket
S3__REGION=eu-central-1
S3__ACCESS_KEY_ID=AKIA...
S3__SECRET_ACCESS_KEY=secret

Multiple Server Instances (Array of Objects)

When you have a dynamic list of servers or upstreams:

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    // Reads UPSTREAM_0_HOST, UPSTREAM_0_PORT, UPSTREAM_1_HOST, UPSTREAM_1_PORT, ...
    upstreams: eg.arrayOfGroups({
      HOST: eg.string(),
      PORT: eg.port(),
      WEIGHT: eg.integer().min(1).max(100).default(1),
    }),
  },
  sources: [{
    UPSTREAM_0_HOST: "server1.internal",
    UPSTREAM_0_PORT: "8080",
    UPSTREAM_1_HOST: "server2.internal",
    UPSTREAM_1_PORT: "8081",
    UPSTREAM_1_WEIGHT: "2",
  }],
});

// env.upstreams → [
//   { HOST: "server1.internal", PORT: 8080, WEIGHT: 1 },
//   { HOST: "server2.internal", PORT: 8081, WEIGHT: 2 },
// ]

Capturing Dynamic Keys (eg.record)

Capture all env vars matching a pattern — useful for HTTP headers, feature flags, or plugin configs:

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    PORT: eg.port().default(3000),

    // Captures HTTP_HEADER_ACCEPT, HTTP_HEADER_X_API_VERSION, etc.
    // Returns { ACCEPT: "json", X_API_VERSION: "v2" } (prefix stripped)
    defaultHeaders: eg.record("HTTP_HEADER_"),

    // Captures all FEATURE_* vars as-is
    features: eg.record(/^FEATURE_/),
  },
  sources: [{
    PORT: "3000",
    HTTP_HEADER_ACCEPT: "application/json",
    HTTP_HEADER_X_API_VERSION: "v2",
    FEATURE_DARK_MODE: "true",
    FEATURE_BETA_DASHBOARD: "false",
  }],
});

// env.defaultHeaders → { ACCEPT: "application/json", X_API_VERSION: "v2" }
// env.features       → { FEATURE_DARK_MODE: "true", FEATURE_BETA_DASHBOARD: "false" }

Conditional Required Fields

Some fields only make sense depending on other settings:

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    QUEUE_BACKEND: eg.enum(["memory", "redis", "sqs"] as const).default("memory"),

    // Only required when QUEUE_BACKEND=redis
    REDIS_URL: eg.url()
      .sensitive()
      .requiredIf((raw) => raw.QUEUE_BACKEND === "redis"),

    // Only required when QUEUE_BACKEND=sqs
    AWS_SQS_QUEUE_URL: eg.url()
      .requiredIf((raw) => raw.QUEUE_BACKEND === "sqs"),
    AWS_ACCESS_KEY_ID: eg.string()
      .sensitive()
      .requiredIf((raw) => raw.QUEUE_BACKEND === "sqs"),
    AWS_SECRET_ACCESS_KEY: eg.string()
      .sensitive()
      .requiredIf((raw) => raw.QUEUE_BACKEND === "sqs"),
  },
});

Environment Profiles

Provide sensible defaults per environment without .env file duplication:

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    LOG_LEVEL: eg.enum(["debug", "info", "warn", "error"] as const),
    DB_POOL_SIZE: eg.integer().min(1).max(50),
    CACHE_TTL_SECONDS: eg.integer().min(0),
    ENABLE_QUERY_LOGGING: eg.boolean(),
  },
  profiles: {
    development: {
      LOG_LEVEL: "debug",
      DB_POOL_SIZE: "2",
      CACHE_TTL_SECONDS: "0",
      ENABLE_QUERY_LOGGING: "true",
    },
    production: {
      LOG_LEVEL: "warn",
      DB_POOL_SIZE: "20",
      CACHE_TTL_SECONDS: "300",
      ENABLE_QUERY_LOGGING: "false",
    },
    test: {
      LOG_LEVEL: "error",
      DB_POOL_SIZE: "1",
      CACHE_TTL_SECONDS: "0",
      ENABLE_QUERY_LOGGING: "false",
    },
  },
  // Reads NODE_ENV automatically; can be overridden with activeProfile: "production"
});

Values from actual env vars always override profile defaults.


Watch Mode (Hot Reload)

Re-validate and reload config when .env files change — ideal for long-running services:

import { watchEnv, eg } from "@yedoma-labs/bylyt-env-guard";

const handle = watchEnv(
  {
    schema: {
      LOG_LEVEL: eg.enum(["debug", "info", "warn", "error"] as const).default("info"),
      FEATURE_FLAG_X: eg.boolean().default(false),
    },
    sources: [".env", ".env.local"],
  },
  ({ env, error }) => {
    if (error) {
      console.error("Config reload failed:", error.message);
      return; // keep running with last valid config
    }
    // Apply new config — env is fully typed
    updateLogLevel(env.LOG_LEVEL);
    console.log("Config reloaded:", { LOG_LEVEL: env.LOG_LEVEL });
  },
);

// Later, e.g. on SIGTERM:
process.on("SIGTERM", () => {
  handle.stop();
  process.exit(0);
});

Generate .env.example

Auto-generate a .env.example file from your schema — always in sync with the code:

import { generateEnvExample, eg } from "@yedoma-labs/bylyt-env-guard";

const schema = {
  NODE_ENV: eg.enum(["development", "production"] as const)
    .describe("Application environment"),
  PORT: eg.port().default(3000)
    .describe("HTTP port")
    .example(8080),
  DATABASE_URL: eg.url().sensitive()
    .describe("PostgreSQL connection string")
    .example("postgres://user:pass@localhost:5432/mydb"),
  API_KEY: eg.string().minLength(32).sensitive()
    .describe("Third-party API key"),
};

console.log(generateEnvExample(schema));

Output:

# NODE_ENV
# Type: enum | Required
# Constraints: values: development, production
# Application environment
NODE_ENV=

# PORT
# Type: port | Optional
# Default: 3000
# HTTP port
PORT=8080

# DATABASE_URL
# Type: url | Optional | Sensitive
# PostgreSQL connection string
DATABASE_URL=postgres://user:pass@localhost:5432/mydb

# API_KEY
# Type: string | Required | Sensitive
# Constraints: minLength: 32
# Third-party API key
API_KEY=

Add a build script to keep it updated:

// package.json
{
  "scripts": {
    "gen:env": "tsx scripts/gen-env-example.ts"
  }
}

Generate Markdown Docs

Generate a markdown reference table for your README or internal docs:

import { generateMarkdownDocs, eg } from "@yedoma-labs/bylyt-env-guard";

const schema = {
  PORT: eg.port().default(3000).describe("HTTP port"),
  DATABASE_URL: eg.url().sensitive().describe("PostgreSQL connection URL"),
  LOG_LEVEL: eg.enum(["debug", "info", "warn", "error"] as const).default("info"),
  API_KEY: eg.string().minLength(32).sensitive().describe("Third-party API key"),
};

console.log(generateMarkdownDocs(schema, { title: "Configuration Reference" }));

Output:

## Configuration Reference

| Variable | Type | Required | Default | Constraints | Description |
| --- | --- | --- | --- | --- | --- |
| `PORT` | port | — | `3000` | min: 1, max: 65535 | HTTP port |
| `DATABASE_URL` | url | ✅ | — | — | PostgreSQL connection URL |
| `LOG_LEVEL` | enum | — | `info` | `debug`, `info`, `warn`, `error` | — |
| `API_KEY` | string | ✅ | — | minLen: 32 | Third-party API key |

Custom Validation & Transforms

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    // Custom validator
    CRON_EXPRESSION: eg.string()
      .validate((val) => {
        const parts = val.trim().split(/\s+/);
        return parts.length === 5 ? null : "must be a valid 5-part cron expression (e.g. '0 * * * *')";
      }),

    // Transform: parse comma-separated allowed origins into a Set
    CORS_ORIGINS: eg.array()
      .separator(",")
      .transform((origins) => new Set(origins)),

    // Transform: uppercase the region
    AWS_REGION: eg.string()
      .default("eu-central-1")
      .transform((r) => r.toLowerCase()),

    // JSON field with typed result
    RATE_LIMIT_CONFIG: eg.json<{ windowMs: number; max: number }>()
      .optional(),
  },
});

// env.CORS_ORIGINS        → Set<string>
// env.RATE_LIMIT_CONFIG   → { windowMs: number; max: number } | undefined

Layered Sources

Sources are merged left to right — later sources win:

import { createEnv, eg } from "@yedoma-labs/bylyt-env-guard";

export const env = createEnv({
  schema: {
    DATABASE_URL: eg.url(),
    PORT: eg.port().default(3000),
  },
  sources: [
    ".env",            // base config (committed defaults)
    ".env.local",      // local overrides (gitignored)
    ".env.production", // production overrides (loaded via CI)
    process.env,       // runtime env vars win over everything
  ],
});

Error Handling

import { createEnv, EnvValidationError, eg } from "@yedoma-labs/bylyt-env-guard";

try {
  const env = createEnv({ schema: { /* ... */ } });
} catch (err) {
  if (err instanceof EnvValidationError) {
    // failures is an array — ALL errors reported at once
    for (const failure of err.failures) {
      console.error(`${failure.field}: ${failure.message} [${failure.code}]`);
    }
    // failure.code is one of: MISSING | INVALID_TYPE | INVALID_URL | INVALID_EMAIL |
    // INVALID_ENUM | INVALID_JSON | INVALID_DATE | INVALID_PROTOCOL |
    // MIN_VALUE | MAX_VALUE | MIN_LENGTH | MAX_LENGTH | PATTERN | CUSTOM
    process.exit(1);
  }
  throw err;
}

Console output when validation fails:

❌ Environment validation failed:

  • DATABASE_URL: is required but missing
  • PORT: must be at most 65535 (got: 99999)
  • API_KEY: must be at least 32 characters (got: ***)

Schema Types Reference

| Builder | Returns | Description | |---|---|---| | eg.string() | string | Any string value | | eg.number() | number | Parsed via Number(), rejects Infinity | | eg.integer() | number | Integer only — rejects decimals and Infinity | | eg.boolean() | boolean | true/1/yes/ontrue, false/0/no/offfalse | | eg.enum([...]) | union type | Must be one of the specified string values | | eg.url() | string | Validated via new URL() | | eg.port() | number | Integer between 1–65535 | | eg.email() | string | Validated email address format | | eg.json<T>() | T (default: unknown) | Parses JSON string | | eg.date() | Date | Parses date string to Date object | | eg.array() | string[] | Split by separator (default: ,) | | eg.group({...}) | object | Reads KEY__SUBKEY env vars as nested object | | eg.arrayOfGroups({...}) | object[] | Reads KEY_0_SUBKEY, KEY_1_SUBKEY, … as array | | eg.record("PREFIX_") | Record<string, string> | Captures all matching env vars |

Field Modifiers Reference

Universal (all types)

| Method | Description | |---|---| | .required() | Mark field as required (default) | | .optional() | Allow undefined value | | .default(value) | Static default; implies optional | | .default(() => value) | Factory default, evaluated at startup | | .sensitive() | Mask value in error messages | | .aliases("OTHER_NAME") | Fallback env var names | | .describe("text") | Description for docs/example generation | | .example(value) | Example value for .env.example | | .validate(fn) | Custom validator — return null on pass, error string on fail | | .transform(fn) | Transform value after validation | | .deprecated("msg") | Log warning when variable is set | | .requiredIf(fn) | Conditionally required based on other raw values |

Type-specific

| Method | Types | Description | |---|---|---| | .min(n) | number, integer | Minimum value | | .max(n) | number, integer | Maximum value | | .minLength(n) | string, email, array | Min length / min item count | | .maxLength(n) | string, email, array | Max length / max item count | | .pattern(re) | string | Must match regex | | .protocols("https", "wss") | url | Restrict allowed URL protocols | | .caseInsensitive() | enum | Case-insensitive value matching | | .separator(str) | array | Item separator (default: ,) | | .of("number") | array | Typed array items: "string" | "number" | "integer" | "boolean" |

createEnv Options

createEnv({
  schema: { ... },              // required
  sources: [...],               // default: [process.env]
  prefix: "APP_",               // strip prefix from all schema keys
  strict: true,                 // warn on unknown prefixed vars
  profiles: { dev: { ... } },   // per-environment defaults
  activeProfile: "production",  // default: process.env.NODE_ENV
})

Contributing

Prerequisites

  • Bun v1.1+
  • Node.js 20 or 22

Setup

git clone https://github.com/yedoma-labs/bylyt-env-guard.git
cd bylyt-env-guard
bun install

# Install git hooks (pre-commit linting, pre-push tests)
npx simple-git-hooks

Development Commands

# Run tests
bun run test

# Run tests in watch mode
bun run test:watch

# Run tests with coverage
bun run test:coverage

# Type-check without emitting
bun run typecheck

# Lint & format check
bun run lint

# Auto-fix lint & format issues
bun run lint:fix

# Build (ESM + CJS + type declarations)
bun run build

Project Structure

src/
├── schema/          # Schema builder, types, validators
├── core/            # Env parser, source resolver, validation orchestrator
├── errors/          # Structured error types
└── utils/           # String coercion, .env file parser
tests/
├── schema/          # Unit tests for schema layer
├── core/            # Unit tests for core layer
├── errors/          # Unit tests for error formatting
├── utils/           # Unit tests for coercion & dotenv parser
└── integration/     # End-to-end tests

Build Output

bun run build produces the following in dist/:

| File | Format | Purpose | |---|---|---| | index.js | ESM | Modern import usage | | index.cjs | CJS | Legacy require() usage | | index.d.ts | TypeScript | Type declarations (ESM) | | index.d.cts | TypeScript | Type declarations (CJS) | | *.map | Sourcemap | Debugging |

Release

Releases are automated via GitHub Actions. To publish a new version:

bun version patch   # or minor / major
git push --follow-tags

The release.yml workflow publishes to npm when a v* tag is pushed.

License

MIT