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).
src/
env/
schema.ts # the contract
index.ts # wiring
config/
development.ts
production.ts1. 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.
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.
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
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
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_PORTconvention and how to override it. - SPA / browser — the same pattern without a file system.
- SSR — splitting public values from server-only secrets.