Loader
loadEnv and loadEnvFile are the two file-loading entry points. resetEnv undoes them.
Signatures
function loadEnv(envPath?: string, options?: EnvLoaderOptions): voidfunction loadEnvFile( envPath: string, override: boolean, precedence?: EnvPrecedence, // default "file-wins"): voidfunction resetEnv(): void
type EnvPrecedence = "file-wins" | "process-wins";
type EnvLoaderOptions = { override?: boolean; // default true — also write into process.env dir?: string; // default cwd() — search root loadSharedEnv?: boolean; // default true — load .env.shared first precedence?: EnvPrecedence; // default "file-wins" — who wins vs process.env};File resolution (when envPath is omitted)
- If
loadSharedEnvistrueand${dir}/.env.sharedexists, load it first. - Try
${dir}/.env.${process.env.NODE_ENV}(e.g..env.development). - If that file does not exist, fall back to
${dir}/.env. - If that does not exist either, do nothing — no throw. A project with no
.envat all is a supported state.
process.env.NODE_ENV = "development";loadEnv();// → .env.shared (if present)// → .env.developmentprocess.env.NODE_ENV = "test"; // no .env.test on diskloadEnv();// → .env.shared (if present)// → .envloadEnv("/etc/secrets.env"); // explicit path skips the resolver, // but .env.shared is still loaded first // unless loadSharedEnv: falseAn explicitly-passed envPath that does not exist still throws — the caller named it, so a typo must be loud. Only the paths loadEnv derives for itself (steps 2–3 above) are optional.
dir and envPath are also rejected outright when they contain a .. segment, so a value derived from config or a CLI flag can’t walk the loader outside its intended directory and pull an unrelated file into process.env. Absolute paths are still accepted — passing one is the caller explicitly naming that file.
Precedence semantics — file vs. the real environment
| Setting | A key in BOTH the file and process.env |
|---|---|
precedence: "file-wins" (default) | The file value replaces the injected one, in the store and (when override) in process.env. |
precedence: "process-wins" | The injected value is authoritative. The file does not replace it in the store and does not write over it. |
// Docker / Kubernetes / CI: the platform owns the environment.loadEnv(undefined, { precedence: "process-wins" });"file-wins"stays the default for the whole v1.x line so a minor bump never changes what a running deployment reads. v2.0 flips the default to"process-wins".- Keys the loader itself wrote earlier in the same run (
.env.shared, or a previousloadEnvFile) are tracked in an internalSetand are NOT mistaken for platform-injected values — file layering keeps working under"process-wins". - Values taken from
process.envare coerced to primitives the same way file values are ("8080"→8080), but WITHOUT quote stripping or${VAR}interpolation.
Override semantics
| Setting | Internal store | process.env |
|---|---|---|
override: true (default) | Typed value (number / boolean / null / string) | Written, then coerced to string by Node |
override: false | Typed value | Untouched |
loadEnv(undefined, { override: false });
process.env.APP_PORT; // undefined (not written)env("APP_PORT"); // 3000 (in store anyway)Layering
APP_NAME="My App"APP_URL="https://example.com"
# .env.productionDB_HOST=prod-db.example.comDEBUG=falseprocess.env.NODE_ENV = "production";loadEnv();
env("APP_NAME"); // "My App" — from .env.sharedenv("DB_HOST"); // "prod-db.example.com" — from .env.productionenv("DEBUG"); // falseIf a key appears in both files, the environment-specific file wins (it loads second, and with override: true writes through).
loadEnvFile — the low-level form
loadEnvFile("/abs/path/to/.env", /* override */ true);loadEnvFile("/abs/path/to/.env", true, "process-wins");Loads exactly one file. Always throws if the path does not exist — it is the primitive and never guesses:
Error: .env file not found at /abs/path/to/.envUse this when you need to load a file outside the standard resolution chain (e.g. a secrets.env somewhere on disk, deferred loading, multiple env files at different paths).
resetEnv
resetEnv();Does:
- Clears every key from the internal store.
- Deletes any
process.envkeys thatloadEnvFilewrote since module load (tracked internally in aSet). - Re-assigns every key in the import-time
process.envsnapshot back toprocess.env.
The net effect is a true “back to t0” for anything the loader added. Keys that callers set directly on process.env (without going through loadEnv / loadEnvFile) are not tracked and survive the reset — the caller owns their own additions.
Common loading patterns
// 1. Boot at startup, defaults.import { loadEnv } from "@mongez/dotenv";loadEnv();// 2. Read-only — populate store but don't touch process.env.loadEnv(undefined, { override: false });// 3. Custom directory (monorepo with env files in a sibling).loadEnv(undefined, { dir: path.resolve(__dirname, "../config") });// 4. Skip the shared layer (rare).loadEnv(undefined, { loadSharedEnv: false });// 4b. Containerised deploy — injected values outrank the baked-in file.loadEnv(undefined, { precedence: "process-wins" });// 5. Multiple files with explicit ordering.import { loadEnvFile } from "@mongez/dotenv";loadEnvFile("/etc/myapp/base.env", true);loadEnvFile("/etc/myapp/local.env", true); // overrides keys from base