Skip to content

Dotenv

A small, zero-dependency .env loader for Node.js. Reads KEY=VALUE lines, coerces values to typed primitives when they look like one (number, boolean, null), supports ${VAR} interpolation between keys, picks the right file based on NODE_ENV, and layers a .env.shared file of defaults underneath.

Highlighted features

Typed primitives, not strings

env(“APP_PORT”, 3000) returns the number 3000 — not the string “3000”. Same for booleans and null.

${VAR} interpolation

DB_URL=postgres://${DB_USER}:${DB_PASS}@${DB_HOST}/db — substitution happens at parse time, resolving from already-loaded keys then process.env. An unresolvable reference throws instead of corrupting the value.

NODE_ENV file picker

Reads .env.<NODE_ENV> automatically and layers .env.shared underneath for cross-environment defaults.

Zero deps, one file

One source file (src/index.ts), no runtime or peer dependencies. Node-only — uses fs and process.

Install

Terminal window
npm install @mongez/dotenv

Zero runtime / peer dependencies. Node-only.

Quick peek

import { loadEnv, env } from "@mongez/dotenv";
loadEnv();
const port: number = env("APP_PORT", 3000); // 3000, not "3000"
const debug: boolean = env("DEBUG", false); // true / false, not "true"
const dbUrl: string = env("DB_URL");

Boot once at process start, then read typed values from anywhere. env() returns real primitives, not the stringified process.env form.

Mental model

ConceptWhere it livesWhat it is
Internal storeModule-level envData objectThe typed view of every loaded key. Read via env() / env.all().
process.env mirrorReal Node process.envOptional write-through, controlled by override. Always stores strings.
Initial snapshotModule-level initialProcessEnvDataCaptured at first import. resetEnv restores keys from this snapshot.
Precedenceprecedence optionWho wins when a key is in both the file and process.env. "file-wins" (default) or "process-wins".

The loader is stateful and module-scoped. One store per Node process. Calling loadEnv twice merges the second file’s keys into the first store rather than starting over.

env(key) reads store → process.env → default, so a platform-injected variable that no file names is still visible.

Scope boundaries

ConcernLives inWhy
Validation / schemazod, valibot, your codeDoesn’t type-check loaded values
Higher-level config (groups, defaults, dot-notation)@mongez/configThis is one slice — the file-loading slice
Browser/cookie/localStorageOther packagesThis is a Node filesystem reader

Quirks worth knowing

  1. process.env always stringifies. Even though the package writes process.env.PORT = 3000, Node’s process.env setter coerces to "3000". Use env() for the typed value.
  2. ${VAR} is parse-time only. Substitutions happen the moment the value is parsed. Later updates to the referenced key do not re-trigger substitution in earlier lines. Forward references (declaring the key later in the same file) are unsupported and throw.
  3. The default precedence is "file-wins", which is the deployment hazard. A .env file baked into a container image replaces the DATABASE_URL / port / secret the platform injected. Pass precedence: "process-wins" in containerised environments. v2.0 flips the default.
  4. env.all() is the file store only — it is not merged with process.env. The fallback lives in env(key).

Where to go next

  • LoaderloadEnv / loadEnvFile, file-picking semantics
  • ParserparseLine / parseValue, type coercion rules
  • Recipes — common patterns