C.W.K.
Stream
Lesson 03 of 05 · published

--env-file + --watch — dotenv + nodemon, Both Built In

~10 min · modern-node, env-file, watch

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Two flags that retire two ubiquitous npm packages. Most projects still install dotenv and nodemon in 2026 because their templates have for years. Stop."

--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. Add --watch-path=./config to also watch directories not in the graph (e.g. config files loaded at runtime).

The Combined Dev Loop

The full "modern Node dev server" in one command:
node --env-file=.env --watch --enable-source-maps server.mjs
  • --env-file=.env — loads environment
  • --watch — restarts on file change
  • --enable-source-maps — better stack traces (Node 22+ has this on by default in many configs)
That replaces nodemon + dotenv + their transitive trees. A new Node service in 2026 has no reason to install either. The package.json script gets shorter: "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

When cwkPippa moved to Node 22, I deleted dotenv and nodemon from the backend, replaced both with built-in flags. The diff was satisfying — one package.json shrunk, the dev script got more honest. "Honest" in the sense of: you can read what's happening from the flags alone. No nodemon.json. No 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.

Code

The before-and-after·bash
# Old way (still works, costs dependencies)
npm install dotenv nodemon
npx nodemon --require dotenv/config server.mjs

# New way (Node 22+, no deps)
node --env-file=.env --watch server.mjs

# In package.json:
#   "scripts": {
#     "dev":  "node --env-file=.env --watch server.mjs",
#     "start": "node --env-file=.env.production server.mjs"
#   }
Real combinations you'll use·bash
# Watch additional directories not in the module graph
node --watch --watch-path=./config server.mjs
# Useful when your code loads YAML/JSON configs at runtime — Node won't
# 'see' them through the import graph, so it needs an explicit path.

# Watch with the test runner combined
node --env-file=.env.test --test --watch
# Tests rerun on every source change, env loaded once at startup

External links

Exercise

Take any existing Node project that depends on dotenv and nodemon. Remove both from package.json. Rewrite the dev script to use --env-file + --watch. Verify everything still works: secrets load, file changes restart the process, tests still pass. Then check the diff on node_modules size with du -sh node_modules — the savings will be visible.
Hint
If your code does require('dotenv').config() or import 'dotenv/config', delete those lines too. The runtime now handles it via the flag. For nodemon's config (nodemon.json, restartable signal), check whether --watch covers it — most simple setups, yes. Watch-path globs nodemon supported can be approximated with multiple --watch-path flags.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.