Nodemon --exec: Architecture Decisions for Automatic Restart with Transpilers
An architecture note on nodemon's --exec flag: requirements, minimal design, trust boundaries, operational checks, failure modes, and when to choose a different tool.
31 Aug 2025, 15:53 UTC

The Problem: Restarting Transpiled Code Without a Build Step
When developing with TypeScript or other compile-to-JS languages, you face a choice: run a separate build step on every change, or execute source files directly through a transpiler like ts-node. The latter keeps iteration fast but requires the runtime process to restart whenever source files change. Nodemon's --exec flag solves this by wrapping any command and restarting it on filesystem events. This article examines the engineering trade-offs of that design.
Requirements
- Automatically restart the Node.js process when watched source files change.
- Support a custom execution command (for example,
ts-node,babel-node, or a custom script) so transpilation happens inline without a separate build. - Preserve the wrapped command's logging, exit codes, and signal handling so debugging and CI integration behave identically to running the command directly.
Smallest Suitable Design
The minimal configuration invokes nodemon with --exec pointing at the desired interpreter:
nodemon --exec "ts-node" src/index.ts
Nodemon's built-in file watcher (using native OS events via chokidar) monitors the working directory by default. It spawns the --exec command as a child process, forwards stdin/stdout/stderr, and restarts that child when a watched file changes. Configuration can stay in CLI flags or a nodemon.json file:
{
"exec": "ts-node",
"watch": ["src"],
"ext": "ts,json",
"ignore": ["node_modules", "dist"]
}
This design avoids a separate build pipeline, a process manager, or a custom wrapper script. The only moving parts are nodemon's watcher and the child process it manages.
Trust and Data Boundaries
Nodemon runs with the same user privileges as the invoking shell. It reads the local filesystem to watch files and spawns the --exec command as a child. It does not open network ports, expose APIs, or elevate privileges. The trust boundary is the developer's machine: any code executed via --exec inherits the developer's environment variables, filesystem access, and network reach. If the wrapped command (e.g., ts-node) loads third-party plugins, those run inside the same boundary. No data leaves the process unless the child process itself sends it.
Operational Checks
Before relying on this setup in a team or CI pipeline, verify:
- Dev dependency installed:
npm ls nodemonshows it underdevDependencies. - Exit-code propagation: Run a failing command (e.g.,
nodemon --exec "node -e 'process.exit(1)'") and confirm nodemon logs the non-zero exit and restarts (or exits if configured with--exitcrash). - Restart logs visible: Edit a watched file; you should see
[nodemon] restarting due to changes...followed by the child's output. - Ignore patterns respected: Create
node_modules/trigger.txtand confirm no restart occurs. Adjustignoreif build outputs (e.g.,dist/) leak into watch paths.
Concrete Example: TypeScript Project with Delay
A typical TypeScript service uses ts-node with a restart delay to debounce rapid saves:
nodemon --exec ts-node --delay 500ms --watch src --ext ts --ignore dist src/server.ts
Where to run: Project root, in a terminal with the virtual environment activated (if any). Permissions: Standard user; no sudo. Placeholders: Replace src/server.ts with your entry point. Expected check: After saving server.ts, the terminal shows a restart banner and the new log output within ~1 second. Risk: If ts-node crashes on syntax error, nodemon will restart repeatedly; the --delay mitigates but does not eliminate CPU spin.
Failure Modes
- Filesystem unavailable: If the watched directory is on a network mount that disconnects, nodemon emits
ENODEVand stops restarting. The process stays down until the mount returns and nodemon is restarted manually. - Rapid restart loop: A crashing
--execcommand (e.g., syntax error in TypeScript) causes immediate restarts. Without--delayor--restart-delay, this consumes CPU and floods logs. Set--delay 1000msor use--max-restarts(via config) to cap attempts. - Missed events on certain platforms: On Linux with
inotifylimits reached, or on macOS with certain editors that use atomic writes, events can be dropped. Nodemon falls back to polling if configured with--legacy-watchor--polling-interval. - Docker bind-mount inconsistency: In containers, filesystem events from the host may not propagate to the container's inotify. Use
--legacy-watch(polling) or a volume driver that guarantees event delivery.
Conditions That Would Change the Design
| Condition | Design Change |
|---|---|
| Containerized development with unreliable event propagation | Switch to --legacy-watch (polling) or use a tool like watchexec with explicit polling interval. |
| Zero-downtime reload required (e.g., HTTP server) | Replace nodemon with a cluster-aware manager (PM2, Node's cluster module) that gracefully drains connections before restart. |
| Multiple independent processes (worker + API + scheduler) | Use a process manager (PM2, Foreman, or a custom orchestrator) instead of a single nodemon instance. |
| Production deployment | Do not use nodemon. Build artifacts (tsc output) and run with a production process manager (systemd, PM2, Kubernetes). |
| Large monorepo with many packages | Scope watch paths per package (--watch packages/api/src) or use a tool like turbowatch that understands workspace boundaries. |
Verification Checklist
Run these quick checks after any configuration change:
# 1. Basic restart verification
nodemon --exec "node -e 'console.log(Date.now())'"
# Edit a watched file; timestamp should update.
# 2. Argument forwarding check
nodemon --exec "node -e 'console.log(process.argv.slice(2).join(","))'" arg1 arg2
# Output should show "arg1,arg2" after each restart.
# 3. Ignore pattern validation
touch node_modules/should-not-trigger.txt
# No restart banner should appear.
Limitations
Nodemon is a development tool. It lacks graceful shutdown hooks, health checks, log rotation, load balancing, and the operational maturity required for production. It also does not coordinate across multiple processes; each nodemon instance manages one child. For anything beyond local development, move to a build + process-manager pipeline.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.