Skip to content
C.W.K.
Stream
Lesson 03 of 05 · published

--env-file + --watch — Built-In Dev Primitives

~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 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

A simple modern Node dev server can be one command:
node --env-file=.env --watch server.mjs
  • --env-file=.env — loads environment
  • --watch — restarts on module-graph changes
That replaces the common dotenv + nodemon setup and its transitive trees. Keep a dedicated watcher when you depend on advanced include/exclude rules, restart hooks, or cross-platform explicit path watching. The package.json script still 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
# Use an explicit watch set (macOS and Windows only)
node --watch-path=./config server.mjs
# --watch-path implicitly enables watch mode and replaces the default
# imported-module watch set; it is unavailable on Linux.

# 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 a simple Node project that uses dotenv and nodemon without advanced nodemon hooks or glob policy. Replace their common roles with --env-file + --watch, remove the imports and dependencies, then verify secrets load, module changes restart the process, and tests pass. If the project needs behavior the built-ins do not provide, document that boundary and keep the dedicated tool.
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.