Quickstart

Wire a typed env in a Node app in under five minutes — schema, per-environment config files, and a validated env object you can import anywhere.

A working setup has three files: the schema (contract), one or more per-environment config files (typed values), and the wiring (defineEnv + loadConfig).

project layout
src/
  env/
    schema.ts          # the contract
    index.ts           # wiring
  config/
    development.ts
    production.ts

1. Define the schema

The schema is the contract. Every variable your app expects is declared here, grouped into branches, and each leaf is a Standard Schema validator that runs at boot.

src/env/schema.ts
import { schema, type Config } from "@vlandoss/env";
import * as z from "zod";

export const Env = schema({
  log: {
    LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"])
  },
  server: {
    HOST: z.string(),
    PORT: z.coerce.number().int().positive()
  },
  db: {
    URL: z.string()
  },
});

export type EnvConfig = Config<typeof Env>;

2. Write a per-environment config file

EnvConfig is type-checked against the schema — typos and wrong types fail at compile time.

src/config/development.ts
import type { EnvConfig } from "../env/schema.ts";

export default {
  log: { LEVEL: "debug" },
  server: { PORT: 3000, HOST: "localhost" },
  db: { URL: "postgres://localhost/dev" },
} satisfies EnvConfig;

3. Wire it up

src/env/index.ts
import { defineEnv } from "@vlandoss/env";
import { loadConfig } from "@vlandoss/env/fs";
import { Env } from "./schema.ts";

const config = loadConfig(Env);
export const env = defineEnv({ schema: Env, config });

loadConfig(Env) synchronously auto-discovers [src/]config/<envName>.{ts,mts,cts,js,mjs,cjs,json} under process.cwd() (no await). defineEnv then merges config with process.env and validates against the schema. If anything is missing or wrong, it throws naming the dot-path of the offending leaf.

4. Read typed values

src/app.ts
import { env } from "./env/index.ts";

console.log(env.server.PORT);  // number
console.log(env.db.URL);       // string
console.log(env.$name);        // "development" | "production" | …
if (env.IS_PROD) { /* ... */ }

The shape of env mirrors your schema exactly: env.server.PORT is number because z.coerce.number() ran during validation, env.db.URL is string. Any path that isn't in the schema (env.cache.TTL, env.server.PROT) is a compile error, so refactors and typos surface in the type checker before they reach a running app.

Where to go next

  • Resolution order — how defaults, config files, and env vars combine.
  • Env-var naming — the SERVER_PORT convention and how to override it.
  • SPA / browser — the same pattern without a file system.
  • SSR — splitting public values from server-only secrets.

On this page