For most Node.js services, node --watch replaces nodemon outright. Stable since Node v22.0.0 and v20.13.0, it restarts when your entry point, anything it imports or an --env-file changes, and it waits for a clean SIGTERM exit. Keep nodemon when you need to watch files your code reads at runtime on Linux, or restart something that is not Node.
Whether that exception applies to you depends on how Node decides what to watch.
What does node —watch actually watch?
By default, the module graph. Node spawns your script as a child process with an IPC channel, the child reports every file it loads through require or import, and the parent watches exactly those files. The CLI docs put it as “the entry point and any required or imported module.”
The swap in package.json is one line:
{
"scripts": {
"dev": "node --watch --env-file-if-exists=.env src/server.js"
}
}
Because the watch list comes from what was loaded, you get some things for free. A JSON file pulled in with import config from './config.json' with { type: 'json' } is part of the graph, so editing it restarts the process. On Node 24 a .ts entry point works too, through native type stripping, and the imported .ts files are watched the same way. We ran both on Node v24.12.0 to check. If you have not dropped ts-node yet, our guide to running TypeScript in Node without ts-node covers the flags.
Env files are watched as well. In the v24.12.0 source, lib/internal/main/watch_mode.js adds every --env-file and --env-file-if-exists path to the watcher, so changing a value restarts the process with the new environment. That pairs well with loading env files without dotenv.
What it misses
Anything your code reads with fs. We started a small HTTP server that imported one module and read a banner.txt with readFileSync at startup. Editing the module restarted the server. Editing banner.txt did nothing, and the server carried on serving the old text.
That covers more files than people expect: SQL files loaded by a query helper, .graphql schemas read at boot, email templates, Handlebars or Liquid views, a YAML config parsed by a library. None of them are in the module graph.
How do I watch files that are not imported?
Use --watch-path. Once you pass it, Node stops filtering by the module graph and restarts on any change under the paths you give:
node --watch-path=./src --watch-path=./templates src/server.js
There are two catches, both in the --watch-path docs.
First, it replaces module watching rather than adding to it: “This will turn off watching of required or imported modules, even when used in combination with --watch.” If your code lives in src/ and your templates in templates/, list both, as above.
Second, and this one bites: “This option is only supported on macOS and Windows.” On Linux it throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM. The source explains why. Directory watching relies on recursive fs.watch, and files_watcher.js only treats win32 and darwin as supporting it.
So a team on Macs can use --watch-path happily, and the same npm run dev fails inside a Linux dev container, on a Linux workstation, or in WSL. If anyone on the team develops on Linux, either stick to the module graph or keep nodemon for that project.
Does node —watch shut down gracefully?
Yes. On a change, the parent sends the child SIGTERM (the default for --watch-kill-signal, per node_options.cc) and waits for the child to exit before starting a new one. Because the old process has fully exited, the classic EADDRINUSE race on restart does not happen, as long as your shutdown actually finishes.
A minimal handler that closes the HTTP server:
import { createServer } from 'node:http';
const port = Number(process.env.PORT ?? 3000);
const server = createServer((req, res) => {
res.end('ok\n');
});
server.listen(port, () => {
console.log(`pid ${process.pid} listening on ${port}`);
});
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
With that handler in place on v24.12.0, each edit logged a restart, the old pid exited, and a new pid bound the same port with no EADDRINUSE. The watcher also applies a 200 ms debounce, so saves that land within 200 ms of each other produce one restart rather than several.
The failure mode is a handler that never exits. We registered a SIGTERM listener that only logged. After 500 ms watch mode printed Waiting for graceful termination... and then sat there. There is no timeout and no fallback to SIGKILL. If your terminal shows that line, something in your shutdown path is holding the event loop open: an unclosed database pool, a setInterval, an open WebSocket. Fix the handler, or press Ctrl+C.
If your framework listens for SIGINT instead, --watch-kill-signal changes the signal. It was added in v24.4.0 and v22.18.0 and is marked Stability 1.1, active development, as of November 2026:
node --watch --watch-kill-signal SIGINT src/server.js
We tested that on v24.12.0. The child received SIGINT and the restart went through.
What does nodemon still do that Node does not?
Going by the nodemon README, five things.
Typing rs and Enter restarts by hand. Node has no equivalent, so you save a file to force a restart.
--exec runs and restarts programs that are not Node, such as nodemon --exec "python -v" ./app.py. Node’s watch mode only ever spawns Node.
-e / --ext and --ignore filter by extension and path. Node has no filter at all: you get the module graph or everything under --watch-path. If a build step writes into a watched folder, the process restarts on every build.
--delay sets how long to wait before restarting. Node’s 200 ms debounce is hard-coded and there is no flag to change it.
-L / --legacy-watch switches to polling. The README calls it a last resort, but on a Docker bind mount or a network share where file system events do not arrive, polling is the only thing that works. Node has no polling mode.
Watch mode also cannot be used with --run, --eval, --check or the REPL, so node --watch --run dev will not watch anything. Point it at a file.
What about the test runner?
node --test --watch reruns only the tests affected by a change. Unlike --watch itself, the test runner’s watch mode is still Stability 1, experimental, as of the v26.13.0 docs, and --watch-path cannot be combined with --test. It is fine for a local loop. Keep CI on a plain node --test.
node —watch vs nodemon: which should you use?
For a Node API or worker whose code is all imported, use node --watch and remove nodemon from devDependencies. Add --env-file-if-exists=.env and a SIGTERM handler that really exits, and that replaces a plain nodemon src/server.js setup.
Keep nodemon if any of these is true: anyone develops on Linux and you need to restart on files read with fs; you run the app from a Docker bind mount that needs polling; you need extension or ignore filters because a build writes next to your source; or the process you are restarting is not Node. Moving off it for the sake of one less dependency is not worth broken restarts for a Linux colleague.
We set up Node.js services and their dev tooling for clients, from the first package.json script to the container it ships in. If your local loop has grown slow or fragile, our Node.js development team can untangle it.