Recipes
Idiomatic compositions of @mongez/dotenv features.
Boot at process start
// src/bootstrap.ts — imported first by your entry pointimport { loadEnv } from "@mongez/dotenv";
loadEnv();import "./bootstrap"; // make sure this runs before anything elseimport { env } from "@mongez/dotenv";
const app = createApp();app.listen(env("APP_PORT", 3000));Per-environment config with shared defaults
APP_NAME="My App"APP_DESCRIPTION="A web app"
# config/.env.developmentDB_URL="mongodb://localhost/dev"DEBUG=trueLOG_LEVEL=debug
# config/.env.productionDB_URL="mongodb+srv://prod-host/app?retryWrites=true&w=majority"DEBUG=falseLOG_LEVEL=infoimport path from "node:path";import { loadEnv, env } from "@mongez/dotenv";
loadEnv(undefined, { dir: path.resolve(__dirname, "../config"),});
env("APP_NAME"); // "My App" (from .env.shared, both envs)env("DEBUG"); // true | false (per-environment)env("DB_URL"); // mongo URL (per-environment)Reading typed values straight into a config object
import { env } from "@mongez/dotenv";
export const config = { app: { name: env("APP_NAME", "App"), port: env("APP_PORT", 3000) as number, debug: env("DEBUG", false) as boolean, }, db: { url: env("DB_URL") as string, },};The as casts are because env(...) returns any. Layer zod / valibot on top if you want runtime validation:
import { z } from "zod";import { env } from "@mongez/dotenv";
const schema = z.object({ APP_PORT: z.number().int().positive(), DEBUG: z.boolean(), DB_URL: z.string().url(),});
export const config = schema.parse({ APP_PORT: env("APP_PORT"), DEBUG: env("DEBUG"), DB_URL: env("DB_URL"),});Loading multiple files in a specific order
import { loadEnvFile } from "@mongez/dotenv";
loadEnvFile("/etc/myapp/base.env", true); // global baseloadEnvFile("/etc/myapp/local.env", true); // host-specific overridesloadEnvFile("./.env", true); // per-checkout overrides lastEach subsequent file with override: true overwrites keys from the previous one.
Read-only mode (don’t touch process.env)
import { loadEnv, env } from "@mongez/dotenv";
loadEnv(undefined, { override: false });
process.env.APP_PORT; // unchangedenv("APP_PORT"); // typed value from the fileUseful when a parent process / orchestrator already set process.env and you want the file as a fallback rather than a replacement.
Distinguishing a loaded null from a missing key
env(key) preserves a deliberately-loaded null (it checks key in envData rather than ??), so the loaded value passes through directly:
import { env } from "@mongez/dotenv";
// .env contains: EST_TIME=nullenv("EST_TIME"); // nullenv("EST_TIME", "fallback"); // null (loaded null wins over default)env("MISSING"); // undefinedenv("MISSING", "fallback"); // "fallback"Reset in tests
resetEnv clears the internal store, deletes any process.env keys that the loader wrote since module load, and re-applies the import-time snapshot — so a single call is enough to undo a loadEnv between tests:
import { afterEach } from "vitest";import { resetEnv } from "@mongez/dotenv";
afterEach(() => { resetEnv();});Keys that the test sets directly on process.env (without going through loadEnv / loadEnvFile) are not tracked by the loader and survive the reset — manage those yourself if needed.
Quoted values with #
The parser is quote-aware: when a value opens with ", ', or `, the matching closing quote is the last occurrence of that same character, and anything after it (whitespace + a # comment) is discarded.
# Fully-quoted, contains #, no trailing commentDB_PASS="AMFSDF#QWEWQE"# → "AMFSDF#QWEWQE"
# Fully-quoted, contains #, with a trailing commentHTTP_URL2="https://${HOST}#fragment" # some comment# → "https://example.com#fragment"
# Unquoted, no # — trailing comment is fineAPP_NAME=My App