"Two built-in flags now cover the common jobs that dotenv and nodemon used to own. Use the runtime primitives when their narrower behavior fits."
--env-file — dotenv Without dotenv
The dotenv package has 30+ million weekly downloads. Its entire job is reading a .env file and putting its contents into process.env. Node 20 added a built-in:
# .env
DATABASE_URL=postgres://localhost/mydb
LOG_LEVEL=debug
API_KEY=sk-something-secret
node --env-file=.env script.mjs
# inside script.mjs, process.env.DATABASE_URL is set
Multiple --env-file flags stack — useful for layered configs:
node --env-file=.env --env-file=.env.local server.mjs
# .env.local overrides .env where keys collide
The format follows the dotenv convention: KEY=value per line, # for comments, quoted values for spaces. The implementation is a thin parser in Node's core; no need to install anything.
--watch — nodemon Without nodemon
nodemon watches your project and restarts Node on file change. Node 22 made --watch stable:
node --watch server.mjs
# Save server.mjs (or any imported module) → automatic restart
It walks the module graph from your entry file and watches every imported file. Edit a deep utility, and the server reloads. Save the entry file itself, same. Use --watch-path=./config when you need an explicit watch set, but note two boundaries: it replaces the default module-graph watch set, and it is supported only on macOS and Windows.
The Combined Dev Loop
node --env-file=.env --watch server.mjs--env-file=.env— loads environment--watch— restarts on module-graph changes
"dev": "node --env-file=.env --watch server.mjs".What About .env.local, .env.production?
The convention from front-end frameworks (Next.js, Vite) loads multiple .env files in order: .env → .env.local → .env.production. Node's --env-file doesn't do this layering automatically — but you can stack flags:
# dev
node --env-file=.env --env-file=.env.local server.mjs
# prod (CI sets this command)
node --env-file=.env --env-file=.env.production server.mjs
For multi-environment setups, write a small wrapper that picks the right combination based on NODE_ENV. The flag itself doesn't pick policy for you — that's its strength (no surprises) and its limitation (you write the policy).
--watch vs Hot Module Replacement
Be clear what --watch does: it restarts the process on change. It does NOT do hot module replacement (HMR) — there's no surgical "reload this module's exports while the process keeps running." For frontend dev servers (Vite), HMR is the right tool. For backend servers, full restart is usually what you want: fresh state, no stale subscriptions, no leaked listeners. HMR for Node servers is rarely worth the complexity.
Pippa's Confession
require('dotenv').config() at the top of every entry. The runtime says what it does. The lesson: every dependency you can delete makes your project a little easier for next-Pippa to read.