mrkeyoor.com_
Wed 05 Aug 05:01 UTC
npmUtilsupdated 05 Aug 2026

dotenv

dotenv reads a .env file from your project root and copies its key=value pairs into process.env, following the twelve-factor rule of keeping config out of code. It is a zero-dependency module, about 2.6 KB gzipped, and one of the most downloaded packages on npm. You call config() once at startup (or preload with dotenv/config) and every process.env read after that sees your local values. Since v16-17 it also ships a small dotenv run CLI and optional dotenvx integration for encrypted .env files.

Verdict

Still the simplest, safest default for loading local .env files, but check first whether your Node version or framework already does this natively. If it does, dotenv is a dependency you do not need.

API stability4/5config() and parse() have barely changed in a decade, but recent majors shipped real behavior changes: v15 changed how # comments parse inside values and v17 turned on runtime log output by default.
Docs4/5The README covers every option with examples and a long FAQ, but there is no separate docs site and a large share of the README now promotes dotenvx rather than documenting dotenv itself.
Maintenance4/5Pushed to within the last day and only 2 open issues and PRs, but it is effectively a one-maintainer project (motdotla) whose attention is split with the commercial dotenvx.
Ecosystem5/5167M weekly downloads and the .env convention it popularized is now assumed by Docker, CI systems, and most Node frameworks; countless tools parse the same file format.

Use it if

  • You run Node below 20.6 (or Bun/Deno scripts via npm compat) and need .env loading, since older Node has no built-in --env-file flag
  • You want the ecosystem-standard convention: nearly every Node tutorial, Docker guide, and teammate already understands a .env file loaded by dotenv
  • You need programmatic control: custom file paths, multiple files with precedence, override behavior, or dotenv.parse() to read env-format strings from anywhere
  • You want zero dependencies in a package that sits in nearly every install tree anyway
Skip it if

Setup reality

npm install dotenv, create .env, call require('dotenv').config() before anything reads process.env. Real friction: since v17 it prints an 'injected env' tip line to stdout by default, which breaks scripts that parse output until you pass quiet: true or set DOTENV_CONFIG_QUIET; in ESM, import hoisting runs your other modules before config() executes, so you need import 'dotenv/config' instead of calling config() yourself; existing env vars are never overwritten unless you pass override: true, which confuses everyone once; and the README is now heavily an advertisement for dotenvx, so finding plain dotenv docs takes scrolling.

Patterns

Load .env into process.env (CommonJS)load-env

// index.js, first line, before anything reads process.env
require('dotenv').config()

console.log(process.env.HELLO)

Anything that reads process.env before config() runs sees undefined; put it at the very top of the entry file.

Load .env in ESM without hoisting bugsesm-import

// ESM: import statements are hoisted, so use the side-effect import
import 'dotenv/config'

import { startServer } from './server.js' // now sees env vars

Calling dotenv.config() manually in ESM runs after your other imports were already evaluated; the dotenv/config side-effect import avoids that.

Load a .env file from a custom locationcustom-path

require('dotenv').config({ path: '/custom/path/to/.env' })

Default resolution is path.resolve(process.cwd(), '.env'), which depends on where you launch the process from, not where the file lives.

Layer multiple .env filesmultiple-files

require('dotenv').config({ path: ['.env.local', '.env'] })

The first file wins for duplicate keys (no overwriting by default); pass override: true to reverse that.

Overwrite variables already set on the machineoverride-existing

require('dotenv').config({ override: true })

By default dotenv never replaces an existing process.env value, so a stale exported shell variable silently beats your .env file.

Silence the v17 startup log linequiet-logging

require('dotenv').config({ quiet: true })
// or: DOTENV_CONFIG_QUIET=true node index.js

Since v17, config() prints an 'injected env' tip to stdout by default; this breaks CLIs whose output gets piped or parsed.

Parse env-format text without touching process.envparse-string

const dotenv = require('dotenv')
const config = dotenv.parse(Buffer.from('BASIC=basic'))
console.log(config) // { BASIC: 'basic' }

parse() only returns an object; it never writes to process.env, which makes it safe for reading env-format files from S3, vaults, etc.

Run any command with .env applied (no code changes)cli-run

npx dotenv run -- node index.js
# pick specific files, first one wins:
npx dotenv run -f .env.local -f .env -- node index.js

The dotenv run CLI arrived in recent v17 releases; flags take precedence over DOTENV_CONFIG_* environment variables.

Store a multiline private key in .envmultiline-values

# .env (works as-is since v15)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
Kh9NV...
-----END RSA PRIVATE KEY-----"

Line breaks inside double quotes are supported since v15; on older versions you must use \n escapes inside one quoted line.

Expand variables inside .env valuesvariable-expansion

// dotenv itself does NOT expand ${VARS}; add dotenv-expand:
const dotenv = require('dotenv')
const dotenvExpand = require('dotenv-expand')

dotenvExpand.expand(dotenv.config())
// .env: DATABASE_URL="postgres://${USER}@localhost/db"

Plain dotenv treats ${USER} as a literal string; expansion needs dotenv-expand or dotenvx.

Alternatives

PackageRegistryPick it when
@dotenvx/dotenvxnpmYou want encrypted .env files, variable expansion, and multi-environment workflows from the same author
envalidnpmYou want your env vars validated and typed at startup instead of silently undefined at runtime
dotenv-flownpmYou want automatic .env.development / .env.production / .env.local layering keyed off NODE_ENV