README.md
July 31, 2026 · View on GitHub
envapt
The apt way to read typed config.
Read config from any source as real typed values, with zero runtime dependencies.
envapt returns config as the type you asked for instead of the string | undefined you get raw, with
a fallback that removes undefined from the return type. It reads from whatever source you bind. On
Node, Bun, and Deno that is process.env and your .env files, bound on import. On Cloudflare
Workers, in the browser, or for a secrets object you fetched at boot, you bind the source with
Envapter.useSource(...).
import { Converters, Envapter } from 'envapt';
const { port, databaseUrl, cacheTtl, allowedOrigins } = Envapter.getRequiredAll(
{
PORT: Converters.Port, // number, checked against 0-65535
DATABASE_URL: Converters.Url, // URL
CACHE_TTL: Converters.Time, // "15m" parsed to 900000
ALLOWED_ORIGINS: Converters.array() // string[]
// ... and many more built-in converters
},
'camelCase'
);
Call that at startup and a missing value fails the boot, with one error naming every key that was
missing. Reads with a fallback (Envapter.getNumber('WORKERS', 4)) never throw.
What you get
- Typed values. A fallback removes
undefinedfrom the return type. Built-in converters cover numbers, integers, floats, booleans, bigint, symbols, JSON, URLs, regular expressions, dates, durations, ports, emails, and arrays, or pass your own function or a Standard Schema validator (zod, valibot, arktype). - Any source. A source is any object with a
readVars()method, so you can bindprocess.env, a Cloudflare Workers binding, a browser bundle, or a secrets payload you fetched from a store at boot. On Node, Bun, and Deno one binds on import. - Zero runtime dependencies. The reader, converters, and built-in
.envparser are self-contained, so nothing is added to your dependency tree. - Runs on Node, Bun, Deno, Cloudflare Workers, and the browser. Node
>=20, Bun>=1.3, Deno>=2.5(ESM and CJS). The portable build resolves through the packageexportsconditions. .envloading built in on Node. The default Node source adds a per-environment file cascade,${VAR}templates, and strict / required checks. Off Node there is no filesystem, so you bind another source withEnvapter.useSource(...)and read with the same typed API.
Install
npm install envapt
pnpm add envapt
yarn add envapt
bun add envapt
deno add jsr:@materwelon/envapt
Quick start
Read values functionally with Envapter, or bind them to class fields with the @Envapt decorator.
Both share the same parsing, converters, and cache.
Functional
Read a value from any call site, in JavaScript or TypeScript. No build step. On Node the source is
bound for you. On Workers and in the browser, call Envapter.useSource(...) first.
import { Converters, Envapter } from 'envapt';
const requestTimeout = Envapter.getUsing('REQUEST_TIMEOUT', Converters.Time, '30s'); // 30000
const maxRetries = Envapter.getNumber('MAX_RETRIES', 3);
const debug = Envapter.getBoolean('DEBUG', false);
On Cloudflare Workers, env is importable at module scope, so bind it once in a config module, and in
the browser seed a PortableSource from the object your bundler injects.
import { env } from 'cloudflare:workers';
import { Envapter, PortableSource } from 'envapt';
Envapter.useSource(new PortableSource(env));
export const apiToken = Envapter.get('API_TOKEN');
Decorator
Bind a value to a class field with a TC39 accessor decorator. No experimentalDecorators flag, and it runs on Bun and Deno from .ts directly.
import { Converters, Envapt, EnvTime } from 'envapt';
class Config {
@Envapt('PORT', { converter: Converters.Port, fallback: 3000 })
static accessor port: number;
@EnvTime('CACHE_TTL', '15m')
static accessor cacheTtl: number;
}
The legacy (experimentalDecorators) decorators are exported from envapt/legacy.
Agent skill
Install the envapt agent skill so AI coding tools use the correct API:
npx skills add materwelonDhruv/envapt
Built by @materwelonDhruv · Apache 2.0