Skip to content

Recipes

Idiomatic compositions of @mongez/dotenv features.

Boot at process start

// src/bootstrap.ts — imported first by your entry point
import { loadEnv } from "@mongez/dotenv";
loadEnv();
src/index.ts
import "./bootstrap"; // make sure this runs before anything else
import { env } from "@mongez/dotenv";
const app = createApp();
app.listen(env("APP_PORT", 3000));

Per-environment config with shared defaults

config/.env.shared
APP_NAME="My App"
APP_DESCRIPTION="A web app"
# config/.env.development
DB_URL="mongodb://localhost/dev"
DEBUG=true
LOG_LEVEL=debug
# config/.env.production
DB_URL="mongodb+srv://prod-host/app?retryWrites=true&w=majority"
DEBUG=false
LOG_LEVEL=info
import 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 base
loadEnvFile("/etc/myapp/local.env", true); // host-specific overrides
loadEnvFile("./.env", true); // per-checkout overrides last

Each 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; // unchanged
env("APP_PORT"); // typed value from the file

Useful 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=null
env("EST_TIME"); // null
env("EST_TIME", "fallback"); // null (loaded null wins over default)
env("MISSING"); // undefined
env("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.

Terminal window
# Fully-quoted, contains #, no trailing comment
DB_PASS="AMFSDF#QWEWQE"
# → "AMFSDF#QWEWQE"
# Fully-quoted, contains #, with a trailing comment
HTTP_URL2="https://${HOST}#fragment" # some comment
# → "https://example.com#fragment"
# Unquoted, no # — trailing comment is fine
APP_NAME=My App