nodemon
nodemon is a command line wrapper for node. You type nodemon server.js instead of node server.js, it starts your app, watches the working directory for file changes, and when something changes it kills the process and starts it again. Nothing in your code changes; it is a supervisor, not a library, and your app never knows it is there. The interesting parts are the details around that loop: which extensions count as a change, which directories are ignored, how long it waits before restarting, which signal it sends so you can clean up, and how to make it drive something that is not node at all. Because it just reads the file extension and looks up an executable, nodemon --exec 'python -v' app.py watches Python files and restarts a Python process the same way. It also exposes the same functionality as a Node module and emits events you can hook.
nodemon still does its one job well and its ignore and signal handling remain more configurable than anything built in. For a new plain-JavaScript project on current Node, node --watch is one flag and zero dependencies, and for TypeScript, tsx watch replaces both nodemon and ts-node.
Use it if
- You are on an older Node version, or a mixed team where some machines are, and you want one restart-on-change command that behaves identically everywhere
- You need to restart something that is not node: a Python script, a Ruby process, a compiled binary, a shell pipeline, all through --exec and execMap
- You need precise control over the watch loop: multiple --watch paths, quoted glob ignores, a --delay to batch rapid saves, or --signal SIGHUP so your app can drain connections before dying
- You want to trigger side effects on restart, such as a desktop notification or a cache clear, through the events config or the module API
- Your project already has a nodemon.json full of tuned settings and the cost of proving a replacement behaves the same is higher than the cost of keeping it
- You are on Node 18.11 or newer and running plain JavaScript. node --watch is built in, has no dependencies, and needs no config file; node --watch-path=src server.js covers most of what people install nodemon for
- You are running TypeScript. nodemon only restarts a process, it does not compile anything, so you still need ts-node or tsx underneath. tsx watch does the watching and the transpiling in one tool and starts faster
- You are on Bun or Deno. Both ship --watch natively and nodemon adds a layer that fights their own module resolution
- You expect state to survive a save. This is a full process restart, not hot module replacement: in-memory caches, websocket connections, and warm database pools all die on every keystroke that hits a watched file. On a slow-booting app that is several seconds of dead time per save
- You want a small dependency footprint. A dev-only restart tool pulls ten runtime dependencies including chokidar 3, semver, minimatch, touch, and pstree.remy, plus simple-update-notifier which prints upgrade nags in your terminal
- You are reaching for it in production. nodemon is a development tool with no restart backoff, no clustering, and no log rotation; if a crash loop starts it will happily restart forever. Use a real supervisor there
- You are watching a Docker bind mount or a network drive. Filesystem events often do not propagate, and the documented fix is --legacy-watch, which polls every file it can find and burns CPU continuously
Setup reality
npm install --save-dev nodemon puts the binary in node_modules/.bin, which is not on your PATH, so you run it through an npm script or npx and not by typing nodemon. Defaults then decide most of your experience: it watches the entire working directory recursively, restarts on .js, .mjs, .cjs, .json, .coffee, and .litcoffee changes, and ignores .git, node_modules, bower_components, .nyc_output, coverage, and .sass-cache. That default extension list is why people think it is broken when editing .ts, .env, or template files, and why a project that writes JSON to disk at runtime restarts itself in a loop. Config lives in nodemon.json in the project or your home directory, or under a nodemonConfig key in package.json, but not both: if a nodemon.json exists or you pass --config, the package.json block is ignored entirely. Ignore patterns are matched against the full absolute path and must use ** rather than a single *, so --ignore '*/test/*' silently matches nothing while --ignore '**/test/**' works, and every glob has to be quoted so your shell does not expand it first. There is a default execMap that maps .ts to ts-node, which is removed automatically if NODE_OPTIONS contains --loader or --import, so TypeScript behaviour changes depending on an environment variable you may not have set yourself. Restarts are delivered as SIGUSR2, so graceful shutdown means listening for that signal with on and not once. Inside containers and on mounted volumes you will likely need legacyWatch, and the throttle before checking for changes is one second by default, adjustable with --delay.
Patterns
Start an app and restart on savebasic-usage
npm install --save-dev nodemon
npx nodemon server.js
# package.json
{
"scripts": {
"dev": "nodemon server.js"
}
}A local install is not on your PATH, so plain nodemon in a terminal either fails or silently runs a different global version. Omit the filename and nodemon reads main or scripts.start from package.json.
Narrow what triggers a restartwatch-and-ignore
nodemon --watch src --watch config \
--ignore '**/*.test.js' \
--ignore '**/fixtures/**' \
-e js,json,ts \
src/server.jsIgnore patterns match the full absolute path and need ** rather than a single *, so --ignore '*/test/*' matches nothing at all. Quote every glob or your shell expands it before nodemon sees it. Narrowing --watch is also the cheapest fix for high CPU on a large repo.
Move the flags into nodemon.jsonconfig-file
// nodemon.json
{
"watch": ["src", "config"],
"ext": "js,json,ts",
"ignore": ["**/*.test.js", "**/fixtures/**"],
"delay": 2500,
"env": { "NODE_ENV": "development" },
"exec": "node --enable-source-maps src/server.js"
}delay in a config file is always milliseconds, while --delay 2.5 on the command line means seconds. If nodemon.json exists, any nodemonConfig block in package.json is ignored completely rather than merged.
Keep the config in package.json insteadpackage-json-config
{
"nodemonConfig": {
"ignore": ["**/test/**", "**/docs/**"],
"delay": 2500
}
}Same keys as nodemon.json, one less file. Only read when there is no nodemon.json and no --config flag, which is a common cause of settings that appear to be ignored.
Restart something that is not nodenon-node-scripts
nodemon --exec 'python -u' app.py
nodemon --exec 'go run .' --ext go
nodemon --exec 'php -S localhost:8000' --ext php
// nodemon.json, for extensions it does not know
{
"execMap": { "pl": "perl", "rs": "cargo run --" }
}With --exec and a target file, nodemon watches that file's extension instead of .js. There is a built-in execMap for py, rb, and ts, and the ts entry is deleted automatically when NODE_OPTIONS contains --loader or --import, which changes behaviour without you touching any config.
Run TypeScript in developmenttypescript-dev-loop
// nodemon.json
{
"watch": ["src"],
"ext": "ts,json",
"exec": "node --import tsx src/server.ts"
}
# or skip nodemon entirely
npx tsx watch src/server.tsnodemon does not compile anything; it only restarts a process, so a TypeScript setup is always nodemon plus a loader. tsx watch does both and restarts faster because it does not respawn a fresh Node process for the type-stripping layer.
Clean up before the process is killedgraceful-shutdown
// nodemon sends SIGUSR2 on restart
process.on('SIGUSR2', () => {
server.close(() => {
db.end().then(() => process.kill(process.pid, 'SIGTERM'));
});
});
# or pick a different signal
nodemon --signal SIGHUP server.jsUse on and not once: nodemon can send the signal again if the first shutdown takes too long. Your handler must eventually kill the process itself, otherwise nodemon waits and the restart appears to hang. The signal goes to every process in the tree, including cluster workers.
Make it work in a container or on a mounted drivedocker-and-mounts
nodemon --legacy-watch --polling-interval 1000 src/server.js
// nodemon.json
{ "legacyWatch": true, "watch": ["src"] }Filesystem events usually do not cross a Docker bind mount or a network share, so nothing restarts and the tool looks broken. Polling fixes it and costs continuous CPU proportional to the number of files watched, which is why you narrow watch paths at the same time.
Run a command when nodemon changes staterestart-events
// nodemon.json
{
"events": {
"restart": "echo restarted >> .dev.log",
"crash": "osascript -e 'display notification \"app crashed\"'"
}
}Handy for clearing a build cache or pinging a reload endpoint on each restart. The command runs in a shell, so anything slow here is added to every restart cycle.
Restart by hand without stopping nodemonmanual-restart
# in the terminal running nodemon
rs<Enter>
// change the trigger word, or turn it off
{ "restartable": "go" }
{ "restartable": false }Useful when a change happens outside the watched tree, such as a database migration or a .env edit. Set restartable to false if your app reads stdin, otherwise nodemon eats the input looking for the trigger word.
Drive nodemon from a Node scriptmodule-api
const nodemon = require('nodemon');
nodemon({ script: 'server.js', ext: 'js json', stdout: false })
.on('start', () => console.log('up'))
.on('restart', (files) => console.log('changed', files))
.on('quit', () => process.exit());
nodemon.on('readable', function () {
this.stdout.pipe(fs.createWriteStream('out.log'));
});Set stdout: false or the child output goes straight to the console and the readable event never gives you anything to pipe. The module keeps the process alive, so handle quit yourself or your script will not exit.
Check whether you still need itnode-watch-comparison
# built into Node 18.11 and later
node --watch server.js
node --watch-path=./src --watch-path=./config server.js
node --watch --env-file=.env server.jsThe built-in watcher tracks files your app actually imports rather than a directory glob, which means fewer spurious restarts and no config file. What it does not give you is ignore patterns, a restart delay, a custom signal, or restarting non-node processes; those are the reasons left to keep nodemon.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| tsx | npm | Your app is TypeScript or ESM and you want transpiling and watching from one process instead of nodemon plus ts-node |
| pm2 | npm | You need this in production: restart backoff, clustering, log management, and startup on boot rather than a dev-time file watcher |
| onchange | npm | You want to run an arbitrary command on file changes without the node-process wrapper assumptions nodemon makes |
| concurrently | npm | The real problem is running a watcher, a compiler, and a server side by side with combined output and one Ctrl-C |