v9.1.0

Introduction

Read environment config as typed values with a fallback, in place of process.env parsed by hand, on Node, Bun, Deno, Workers, and the browser.

You've written Number(process.env.PORT ?? 3000) before. It works until PORT holds something that isn't a port. PORT=80a gives NaN. PORT= gives 0, because its value is an empty string, which ?? leaves alone. If you check a flag with === 'true', DEBUG=1 counts as off. The type says only string | undefined, so every file that reads DATABASE_URL adds its own parse and its own default by hand.

const  = (.. ?? 3000);
const  = .. === 'true';
const  = (.. ?? 60_000);
const databaseUrl = ..;
const databaseUrl: string | undefined

envapt parses the value and applies the default inside the read. Envapter has get for a string and getAs, which takes a converter for any other type. Both take the key first and a fallback last, if you want one.

const port = .('PORT', ., 3000);
const port: number
const = .('DEBUG', ., false); const = .('CACHE_TTL', ., '1m'); const databaseUrl = .('DATABASE_URL', .);
const databaseUrl: URL | undefined

The port call has a fallback, so its type is number. databaseUrl has none, so its type is URL | undefined. You handle the missing case yourself. Converters.Boolean reads 1, yes, true, and on as true, so DEBUG=1 returns true. Converters.Time turns CACHE_TTL=10m into 600000 milliseconds.

The fallback is the value a reader returns when the variable is missing (unset or empty), or when a token or function can't convert its value. PORT=80a and PORT=99999 both return 3000, so the fallback also hides a typo like 80a. A bad array element and a value a schema rejects throw even with a fallback (the full table). For a value the app can't run without, call getRequired or getRequiredAll, which throw.

Reading through envapt is optional, and code that reads process.env directly keeps working. Each read costs an import and a call. Values set only in a .env file reach envapt's readers. They reach process.env too if you import envapt/config (.env files on Node). A script that reads one string can keep reading process.env. envapt helps most once several files read config, because each read gives you the type, the default, and the parse in one call. The compiler also checks every use.

Functions or decorators

Envapter reads one value per call, as above. The @Envapt decorator reads a variable into a class field. A decorated field reads on first access and keeps that value until the cache is rebuilt, for example by a useSource call (When values are read). A reader call converts again each time.

class  {
    @('PORT', { : ., : 3000 })
    static accessor : number;

    @('REDIS_URL', { : ., : new ('redis://localhost:6379') })
    static accessor : ;
}

Use a decorator for settings that many files read, since the class declares each one once and Config.port works anywhere. Use a reader for a one-off read, or for a check that has to run at startup, since a decorated field reads only when something first accesses it. The cost is the accessor keyword on every field, which Decorators explains. Both forms share the same converters and accept a Standard Schema validator such as zod, valibot, or arktype. A decorator takes the schema through its schema option.

Where values come from

A source is the object envapt reads values from. On Node, Bun, and Deno the source is process.env plus your .env files. It binds when you import envapt, so the first read works with no setup.

On Workers, in the browser, and on edge runtimes, envapt reads nothing until you bind a source. Your config arrives there as an object, such as the Workers env or import.meta.env in a bundler like Vite. Wrap that object in a PortableSource and pass it to Envapter.useSource once, before the first read. If a read runs before that, it throws NoSourceBound. Sources shows the full setup.

Where to start

On this page