Reading values
Read one environment variable as a string or through a converter, with a fallback, and see what each reader returns for a bad value.
A reader is a method on Envapter that reads one variable, like get for a string or getAs for a typed value. On Node, readers read process.env and your .env files. You call every reader on Envapter itself, so you construct nothing. Most readers return the value, or undefined when it is missing. The required readers throw. On Workers, the browser, and edge runtimes, every reader throws NoSourceBound until you bind a source with useSource.
Strings and typed values
Without a reader you write the truthy list for flags yourself, and you remember that an empty string means unset. getAs with Converters.Boolean reads 1, yes, true, and on as true and 0, no, false, and off as false, in any case. get returns the value as a string, and getAs returns the type its converter produces. Both take a key and an optional fallback.
const = .('APP_NAME', 'envapt');
const = .('SAMPLE_RATE', ., 0.25);
const = .('DEBUG', ., false);
const maxUploadBytes = .('MAX_UPLOAD_BYTES', ., 0n);0n is the fallback, so maxUploadBytes is a plain bigint. sampleRate uses Converters.Number because 0.25 is a fraction. That converter also accepts 3.5 or 99999 as a port. Converters.Port takes only whole numbers from 0 to 65535, and Converters.Integer takes only whole numbers. Converters lists the rest.
Fallbacks and missing values
Without a fallback, a reader's type includes undefined, so you handle that at every call site. A fallback removes undefined from the type.
const withFallback = .('API_URL', 'http://localhost');
const noFallback = .('API_URL');withFallback is a string because 'http://localhost' supplies a value when API_URL is missing. noFallback can be undefined.
A variable is missing when it is unset or empty. In strict mode (Envapter.strict = true) a value of only spaces is missing too. With strict mode off, it counts as a value, so Converters.Number reads it as 0 (Required values covers strict mode).
The readers differ when a value is set but can't convert.
| Reader | Result for a value it can't convert |
|---|---|
getAs with a built-in converter | the fallback, or undefined without one |
getAs with Converters.array | throws ArrayElementConversionFailed for a bad element, even with a fallback. In strict mode an empty element throws EmptyArrayElement |
getAs with your own function | if the function returns undefined or null, getAs returns the fallback, or undefined without one. An error it throws reaches your code unchanged |
getAs with a schema | throws a schema error such as SchemaValidationFailed, and the fallback is not used |
getRequired, getRequiredAll | throws InvalidEnvValue |
A fallback can hide a typo. The sampleRate read above returns 0.25 for SAMPLE_RATE=high, and nothing reports it unless you set Envapter.debug = 'verbose' (debug logging). If a wrong value has to stop the app, read it with getRequired instead.
If a fallback has the wrong type for its converter, TypeScript reports it before you run the code, as with 'abc' for Converters.Port. A time fallback is milliseconds or a string like '15m', so '5' is an error too. In plain JavaScript, or when a cast gets past the types, the call throws instead. The error is FallbackConverterTypeMismatch or MalformedTimeFallback, and it throws even when the variable is set. Errors and debugging has every code.
Ordered keys
Every reader except getRequiredAll accepts a list of keys. The reader uses the first key that has a value, so a renamed variable can keep its old name as a backup in the same read. It skips a key that is unset or empty, while ?? skips only an unset one.
// DATABASE_URL if it has a value, else the name older deploys still set
const databaseUrl = .(['DATABASE_URL', 'LEGACY_DATABASE_URL'], .);If you declare the list on its own, it widens to string[], which a reader's key parameter rejects. Write the list inline, or add as const.
const keys = ['DATABASE_URL', 'LEGACY_DATABASE_URL'] as const;Other types with getAs
Without getAs, you check for a missing variable, apply the default, and call new URL(raw) yourself, which throws on a bad value. getAs takes a converter, which can be:
- a built-in converter such as
Converters.UrlorConverters.Time. Converters.array(...), which splits a value on commas into a list, converting each element with theofconverter you pass.- your own
(raw: string) => Tfunction, which runs only when the variable has a value. - a Standard Schema validator, such as one from zod, valibot, or arktype.
const origins = .('ALLOWED_ORIGINS', .({ : . }));
function (: string) {
const [ = '', = '32'] = .('/');
return { , : () };
}
const trustedProxies = .('TRUSTED_PROXY_CIDR', , { : '127.0.0.1', : 32 });origins is a URL[], or undefined while ALLOWED_ORIGINS is unset. parseCidr turns 10.0.0.0/8 into { network: '10.0.0.0', bits: 8 }, a shape no built-in converter produces.
The raw string
Every other reader expands ${NAME} references inside the value (Templates explains the syntax). Expansion breaks a value your own code sets later, like an email subject holding ${ORDER_ID}. If a variable named ORDER_ID exists, envapt substitutes its value. In strict mode, a reference to a missing variable throws. getRaw returns the string exactly as stored. It takes a key or an ordered key list and no fallback. To default an unset key, write Envapter.getRaw(key) ?? 'default'. It returns undefined when the key is unset and an empty string when the variable is empty.
// ORDER_EMAIL_SUBJECT=Your order ${ORDER_ID} has shipped
const subject = .('ORDER_EMAIL_SUBJECT');subject keeps the ${ORDER_ID} text, ready for your own code to replace.
Extending Envapter
Every reader is a static method, so a class that extends Envapter inherits all of them. You call them as Config.get(...). A subclass is worth writing when one setting combines several reads. Decorated fields work on any class, so a config class that holds only decorated fields doesn't need to extend Envapter.
class extends {
static get (): string {
const = .('HOST', 'localhost');
const = .('PORT', ., 3000);
return `http://${}:${}`;
}
}
.origin;Config.origin converts again on every access, while a decorated field keeps the value from its first read until the cache is rebuilt.
Envapter stores strict, the bound source, and the cache, so a subclass shares them. If you set Config.strict = true, strict mode turns on for every read in the process.
More readers
- Required values:
getRequiredandgetRequiredAllthrow when a value is missing.requirethrows if any listed key has no value, andhasreturns whether a key has one. - Templates:
resolvebuilds a string from several keys. - Environment:
isProduction,isDevelopment, and the other environment checks.
Structuring your config
Read your app's variables once, in one typed config module that throws at boot on a bad deploy, then import the values from every other file.
Converters
Turn a raw environment string into a number, URL, duration, list, or any type you need, with a built-in token or your own function.