// the apt way to read config

Typed config,
from any source.

Read config as real types from process.env, a Workers binding, a browser bundle, or any object you bind. It has no runtime dependencies, and it accepts zod, valibot, and arktype schemas.

Node 22+Bun 1.3+Deno 2.5+WorkersBrowserESM only0 dependencies
config.ts
import { Envapter, Converters } from 'envapt';

// a port, with a fallback
const port = Envapter.getAs('PORT', Converters.Port, 3000);

// ordered keys, first defined wins
const db = Envapter.getAs(
  ['DATABASE_URL', 'DB_URL'],
  Converters.Url
);

// typed lists
const origins = Envapter.getAs('CORS_ORIGINS',
  Converters.array({ of: Converters.Url })
);

Every value, converted and typed.

Pick a converter and read the value back as its real type. Each one ships with envapt.

env.ts
Envapter.getAs('CACHE_TTL', Converters.Time)
CACHE_TTL=15m
↳900000number

Or pass your own (raw: string) => T function, or validate through a Standard Schema validator (zod, valibot, arktype). All converters.

Stop at startup, with every bad key.

getRequiredAll reads a group of variables in one call. If any are missing, it throws one error that lists every missing key. If they're all set but some can't be converted, one error lists those.

config.ts
import { Envapter, Converters } from 'envapt';

export const config = Envapter.getRequiredAll({
  DATABASE_URL: Converters.Url,
  PORT: Converters.Port,
  CACHE_TTL: Converters.Time
});
terminal
$ DATABASE_URL=postgres://db/app \
  PORT=99999 CACHE_TTL=soon node config.ts

EnvaptError [210]: Validation failed for PORT, CACHE_TTL.
    at …
  code: 210,
  issues: [
    { message: 'could not be converted', path: [ 'PORT' ] },
    { message: 'could not be converted', path: [ 'CACHE_TTL' ] }
  ]
}

Each issue gives the key and why it failed. Required values.

Two ways to read, one engine.

Read values with Envapter, or declare them as class fields with @Envapt. Both share the same parsing, converters, and cache.

app.ts
import { Envapter, Converters } from 'envapt';

const config = {
  port: Envapter.getAs('PORT', Converters.Port, 3000),
  dbUrl: Envapter.getAs('DATABASE_URL', Converters.Url)
};
config.ts
import { Envapt, Converters } from 'envapt';

class Config {
  @Envapt('PORT', {
    converter: Converters.Port,
    fallback: 3000
  })
  static accessor port: number;
}

A fallback removes undefined from the return type.

Bind a source, read it typed.

A source is what envapt reads variables from. On Node, Bun, and Deno, importing envapt binds one that reads process.env and your .env files. On Cloudflare Workers, in the browser, or for secrets you fetch from a store at boot, you bind one yourself in a single line and read with the same typed API.

worker.ts
import { env } from 'cloudflare:workers';
import { Converters, Envapter, PortableSource } from 'envapt';

Envapter.useSource(new PortableSource(env));

const api = Envapter.getAs('API_BASE_URL', Converters.Url);
main.ts
import { Converters, Envapter, PortableSource } from 'envapt';

// Vite inlines import.meta.env at build time
Envapter.useSource(new PortableSource(import.meta.env));

const beta = Envapter.getAs('VITE_BETA', Converters.Boolean, false);

Workers · Browser · Sources · Secret stores

Browser values end up in your bundle, where anyone can read them. Keep secrets on the server.

On Node, Bun, and Deno, envapt loads .env.

In production, envapt reads .env.production.local, then .env.production, then .env.local, then .env. A higher file wins over a lower one. By default, a variable set in the shell wins over every file. envapt skips a missing file, and skips the cascade when the bound source has no files.

load order · most-specific wins

  1. .env.production.localwins
    DATABASE_URL=pg://${DB_HOST}:5432/app
  2. .env.production
    DB_HOST=prod-db
  3. .env.localmissing, skipped
  4. .envbase

resolved

Envapter.get('DATABASE_URL');
// pg://prod-db:5432/app

${DB_HOST} expanded into the URL.

Need the values on process.env?

import 'envapt/config';

Read config from any source, fully typed.

Install it, read your first value typed, and bind a source when you leave Node.