Migrating from a getEnv helper
Graduate from a 12-line presence check helper to typed coercion and boundary enforcement.
ArkEnv is designed to replace a handwritten getEnv helper. ArkEnv
parses and coerces environment variables from one schema, and framework
plugins keep server secrets off the client.
A presence check is often the first helper a team writes. It installs
nothing and types every key as string. It holds while every variable
is a required string. Booleans, ports, and a browser bundle are where
it fails quietly.
The DIY helper
Most handwritten helpers check whether required keys exist in process.env:
export function getEnv() {
const { PORT, DEBUG, DATABASE_URL, STRIPE_SECRET } = process.env;
if (
PORT === undefined ||
DEBUG === undefined ||
DATABASE_URL === undefined ||
STRIPE_SECRET === undefined
) {
throw new Error("Missing required environment variables");
}
return { PORT, DEBUG, DATABASE_URL, STRIPE_SECRET };
}This helper works when all variables are required strings. The breakdown happens when values need coercion or client/server separation.
Failure classes comparison
The same four inputs, and what each approach does with them:
| Failure class | Helper | ArkEnv |
|---|---|---|
Boolean (DEBUG=false) | Truthy string "false" | Coerces to boolean false |
Number (PORT + 1) | Concatenation "30001" | Integer arithmetic 3001 |
Empty strings (DATABASE_URL=) | Presence check passes; returns "" | Fails the string.url rule at boot (invalid URL), not a generic presence / “missing” check. Note: emptyAsUndefined is opt-in; default does not treat "" as missing. |
| Client/server secrets | No boundary; a server secret imported on the client can leak or silently go undefined | Framework plugins enforce the public prefix and block server-key access / server-schema import on the client |
Where the helper breaks
Boolean coercion
In JavaScript, any non-empty string is truthy—including "false":
function getEnv() {
const debug = process.env.DEBUG;
if (debug === undefined) {
throw new Error("missing DEBUG");
}
return { debug };
}
const { debug } = getEnv();
if (debug) {
// always runs when DEBUG is set — including DEBUG=false
}ArkEnv automatically coerces "true" and "false" into boolean primitives before validation.
Numeric operations
Because process.env only holds strings, numeric operations without coercion lead to concatenation:
const { port } = getEnv();
// port: string
listen(port);
port + 1; // "30001"
port > 1024; // lexicographic, not numericArkEnv parses integers and numbers directly, validating ranges (number.port or 0 <= number <= 65535) at boot.
Empty string values
When an environment variable is set to an empty string in .env:
DATABASE_URL=A helper checking === undefined lets "" pass without error. In ArkEnv, declaring DATABASE_URL: "string.url" fails the URL validation rule at boot because "" is not a valid URL. Note that ArkEnv defaults to preserving empty strings; ignoring them requires the opt-in emptyAsUndefined: true option.
Client/server secret boundary
When a helper exports STRIPE_SECRET from a shared module:
export const env = getEnv();
// env.STRIPE_SECRET accessed in a client componentBundlers importing this helper into the browser either bundle the plaintext secret or evaluate it as undefined. ArkEnv's framework plugins (Next.js, Nuxt, Vite, Bun) enforce client prefixes (such as NEXT_PUBLIC_ or NUXT_PUBLIC_) and block server keys from leaking to browser bundles.
Graduating to ArkEnv
Replace the helper with one schema in ./env.ts. The keys stay. The
types stop being strings, and a bad value fails at import.
import from "@arkenv/core";
export const = ({
NAME: "string",
PORT: "number.port",
DEBUG: "boolean = false",
DATABASE_URL: "string.url",
: "'development' | 'production' | 'test' = 'development'",
});
.PORT; // number
.DEBUG; // booleanTo scaffold ArkEnv in your project:
npx arkenv init