Watch and Build with Nodemon
Nodemon is one of the most widely recognized tools among Node.js developers. Almost every technical guide or textbook on Node.js mentions it when discussing live reloading for builds or scripts triggered by file changes. In my previous post, Watch Files in Node Project with Native Features, I explored how to achieve this functionality using Node's built-in capabilities. Admittedly, I have long relied on Nodemon to automatically restart my builds. In this post, I'll compare Nodemon with the native watch mode and the File System API to highlight their strengths and trade-offs.
As a starting point, the two commands below are functionally equivalent:
nodemon build.js
node --watch build.js
For this reason, as in the previous article, I will not spend much time discussing the basic usage of Nodemon. This similarity demonstrates that Nodemon is essentially about live reloading. It is not a unique feature of the tool itself.
I will explore Nodemon through the YouTube play on hover web component. If you want to test the project locally, run the following commands:
git clone --single-branch --branch ytPlayOnHover https://github.com/sangafabrice/2025-10-22.git ytPlayOnHover
cd ytPlayOnHover
npm install
This setup installs Nodemon as a local dependency. If you prefer to install it globally, use:
npm install --global nodemon
Here are the project's build scripts:
package.json
View on GitHub
{
"scripts": {
"build:js": "rollup --config",
"build:css": "postcss **/*.css !**/*.min.css --dir . --base . --ext min.css",
"build:html": "npm run build:html_player && npm run build:html_thumb",
"build:html_player": "cd src/templates/player/assets&& html-minifier-terser template.html --output template.min.html --collapse-whitespace",
"build:html_thumb": "cd src/templates/thumbnail/assets&& html-minifier-terser template.html --output template.min.html --collapse-whitespace",
...
},
...
}
When building the project for the first time, I run:
npm run build:css && npm run build:html && npm run build:js
After the build, the structure of the src directory looks like this:
src
View on GitHub
src
templates
player
assets
shadow.css
shadow.min.css
template.html
template.min.html
volume-mute.svg
volume-title.svg
volume-up.svg
player.css.js
player.dom.js
player.event.js
player.event.load.js
player.event.mutestate.js
player.event.play.js
player.event.reset.js
player.html.js
player.html.render.js
player.html.time.js
progressBar
assets
shadow.css
shadow.min.css
progressBar.css.js
progressBar.event.js
thumbnail
assets
shadow.css
shadow.min.css
template.html
template.min.html
yt-play-icon.svg
thumbnail.css.js
thumbnail.dom.js
thumbnail.event.js
thumbnail.html.js
ytIFrame
ytIFrame.api.ready.js
ytIFrame.converter.js
ytIFrame.event.js
ytIFrame.js
utils
_memo.http.js
_thumbUrl.http.js
_videoIdValidation.http.js
domMap.util.js
duration.util.js
memo.util.js
parseHTML.util.js
reflection.util.js
resolution.util.js
shadowCSS.util.js
thumbUrl.http.js
thumbUrl.util.js
videoIdValidation.http.js
videoUrl.util.js
fttl-progress-bar.js
fttl-yt-player.js
fttl-yt-thumb.js
The minified CSS and HTML files here are stored in the same directories as their source files. However, these minified files are excluded from the GitHub repository to keep it clean and focused on source code only.
1. More In One Command Line
Everything that would normally require several steps when using Node's built-in watch mode can be achieved with a single Nodemon command:
nodemon --watch src/**/* --ignore *.min.* --ext js,css,html,svg --exec "npm run build:css && npm run build:html && npm run build:js"
As shown above, Nodemon provides a more streamlined approach. It supports:
- defining an ignore list,
- using glob patterns for flexible file matching, and
- executing and chaining commands directly without the need for an intermediate script.
This command watches the src directory for changes in files with the extensions .js, .css, .html, and .svg. It ignores all files that contain .min. in their names, preventing unnecessary rebuilds. Whenever a watched file changes, Nodemon automatically executes the chained build scripts in sequence: it first runs the CSS build, then the HTML build, and finally the JavaScript build.
By default, Nodemon only watches JavaScript-related files (.js, .mjs, .coffee, .litcoffee, and .json). To ensure that other file types are also monitored, I explicitly added their extensions to the --ext option. In this project, these extensions represent all the file types in use, so we can simplify the command by replacing the list with *, which tells Nodemon to watch files of any type; we can also shorten the watch pattern at the same time:
nodemon --watch src --ignore *.min.* --ext * --exec "npm run build:css && npm run build:html && npm run build:js"
Note that if Nodemon is installed locally, the command should be executed using npx. However, I chose to omit npx here because it is unnecessary when the command is defined as a package.json script. For instance, you can add the following entry to your project's scripts section and run it directly with npm run watch:
package.json
View on GitHub
{
"scripts": {
"watch": "nodemon --watch src --ignore *.min.* --ext * --exec \"npm run build:css && npm run build:html && npm run build:js\"",
...
},
...
}
We can simplify this even further by moving the chained build commands into a separate script named build, then referencing it in the watch script. This keeps the configuration cleaner and easier to maintain:
package.json
View on GitHub
{
"scripts": {
"build": "npm run build:css && npm run build:html && npm run build:js",
"watch": "nodemon --watch src --ignore *.min.* --ext * --exec npm run build",
...
},
...
}
We can simplify the command to just nodemon if it is executed from the project root containing the following nodemon.json configuration file:
nodemon.json
View on GitHub
{
"exec": [ "npm run", "build"],
"ext": "*",
"ignore": [ "*.min.*" ],
"watch": [ "src" ]
}
If you need to monitor multiple directories or define several ignore patterns that cannot be captured with a single glob expression, you can list them as arrays. This structure directly mirrors how you would use repeated --watch and --ignore flags in the command line.
It is also worth noting that exec can be defined as an array. In that case, it does not represent multiple commands executed sequentially, but rather the individual parts of a single command, as shown above.
For simplicity in this example, we will keep exec, ignore, and watch as plain strings, as shown in the snippet below, especially for the 2 last properties since they are single-valued. I prefer to merge the Nodemon configuration directly into the package.json file. This keeps everything in one place:
package.json
View on GitHub
{
"scripts": {
"watch": "nodemon",
...
},
"nodemonConfig": {
"exec": "npm run build",
"ext": "*",
"ignore": "*.min.*",
"watch": "src"
},
...
}
Force Restart
When you run the Nodemon command, it displays a message indicating that you can manually restart the process at any time, even if no files have changed:
$ nodemon
[nodemon] 3.1.10
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src\**\*
[nodemon] watching extensions: (all)
[nodemon] starting `npm run build`
Once the build completes, you can type rs and press Enter to force Nodemon to restart the command and trigger a rebuild.
2. Use the Restart Event Callback
Just like with the fs.watch method, Nodemon can also provide the name of the modified file that triggered a restart.
A major advantage, however, is that Nodemon allows defining a restart event callback directly inside its configuration as an advanced feature that exposes an environment variable called FILENAME, containing the path of the modified file.
For example, if you simply want to display the modified file in the console, you can configure Nodemon as follows:
package.json
View on GitHub
{
"nodemonConfig": {
"events": {
"restart": "node --eval=\"console.log(require('process').env.FILENAME)\""
},
...
},
...
}
The Node command is used here because it works across platforms. To better understand it, here's how the same output can be achieved in different environments:
In Node:
node --eval="console.log(require('process').env.FILENAME)"
In POSIX Bash:
Bash
echo $FILENAME
In Windows Command Prompt:
BatchFile
echo %FILENAME%
All of these commands print the name of the file that caused Nodemon to restart. The only difference is that, on Windows, environment variable names are not case-sensitive.
3. An Example of Selective Rebuilds Using FILENAME
The Setup
In this section, we will modify the src directory structure by adding a bin folder inside each assets folder. These bin directories will store all the preprocessed CSS and HTML files.
For example, the structure of the player/assets directory will look like this:
src
View on GitHub
src/templates/player/assets
bin
shadow.min.css
template.min.html
shadow.css
template.html
volume-mute.svg
volume-title.svg
volume-up.svg
Creating multiple bin folders directly from the command line using tools such as postcss-cli or html-minifier-terser is not straightforward. It requires writing complex command chains that often depend on the developer's operating system (Windows or POSIX).
For example, saving the minified template.html file from the player/assets directory into its corresponding bin folder would involve different commands on each platform (build:html_player:win and build:html_player:posix). This is because html-minifier-terser cannot both create directories and change file extensions (from .html to .min.html) in a single operation.
On Windows Command Prompt, you might use:
BatchFile
cd src/templates/thumbnail/assets&& (dir bin > nul 2>&1 || mkdir bin)&& html-minifier-terser template.html --output bin/template.min.html --collapse-whitespace
While on POSIX Bash, the equivalent command would be:
Bash
cd src/templates/thumbnail/assets&& (ls bin > /dev/null 2>&1 || mkdir bin)&& html-minifier-terser template.html --output bin/template.min.html --collapse-whitespace
These differences make maintaining a cross-platform build setup cumbersome, which is why a Node.js-based script is often a more reliable and portable solution.
The Scripts
To handle selective rebuilds, I created a module that exports a function responsible for minifying or preprocessing an array of modified template files (both CSS and HTML).
auto/minify.js
View on GitHub
import fs from "fs/promises";
import path from "path";
async function minifyTemplate(template) {
const outDir = getBinDir(template);
const minTemplateFile = getMinTemplatePath(template, outDir);
const templateType = path.extname(template);
return fs.mkdir(outDir, { recursive: true })
.then(() => fs.readFile(template, { encoding: "utf8" }))
.then(minifyContent.bind(null, templateType))
.then(fs.writeFile.bind(fs, minTemplateFile));
}
export default function minifyTemplates(templates) {
const minifiers = [];
minifiers.push(excludeNonTemplate(templates).map(minifyTemplate));
return Promise.all(minifiers);
}
...
The function minifyTemplate() takes a file path, determines its corresponding output directory (bin), and ensures the folder exists. It then reads the file, passes its content to the appropriate minifier function based on the file type, and writes the processed output to the bin directory. The minifyTemplates() function simply filters out any files that are not CSS or HTML, then calls minifyTemplate() for each valid template file.
The functions that specifically handle CSS and HTML minification remain private utilities within the auto/minify.js module, maintaining separation of concerns between the preprocessing logic and the build orchestration.
Next, this module is imported into build.js, which Nodemon restarts automatically whenever a monitored file changes:
build.js
View on GitHub
import minifyTemplates from "./auto/minify.js";
import { env } from "process";
await minifyTemplates([
env.FILENAME ??
getAllTemplates()
].flat());
...
The build.js script retrieves the modified file path from the environment variable FILENAME, which Nodemon sets on each restart. If no file is modified (for example, when the watcher starts for the first time), it defaults to processing all template files by calling getAllTemplates(). Using an array ensures flexibility in both scenarios, single file changes or full builds.
This script introduces a more targeted preprocessing workflow that rebuilds only the modified file.
To complete the build pipeline, the preprocessed templates and JavaScript files are then bundled together into a single JavaScript file using a Rollup configuration. Since the Rollup configuration itself is a JavaScript module, it can directly import the preprocessing script to ensure that template minification finishes before the bundling process begins:
rollup.config.js
View on GitHub
import "./build.js";
...
In this setup, the build:js script effectively becomes the main build script, because it now handles both preprocessing and bundling. The command, however, remains unchanged in the package.json:
package.json
View on GitHub
{
"scripts": {
"build": "rollup --config",
...
},
...
}
Alternatively, you might prefer to keep preprocessing and bundling more clearly separated.
In that case, the build.js script could spawn a child Node.js process to execute the bundling command (npm run build:js) after the templates are processed.
The Configuration
Building on the previous setup, I can now refine our Nodemon configuration to automatically restart the build script whenever a file changes as follows:
package.json
View on GitHub
{
"scripts": {
"build": "rollup --config",
"watch": "npm run build && nodemon"
},
"nodemonConfig": {
"events": {
"restart": "npm run build"
},
"exec": "node -e \"\"",
...
},
...
}
Initially, we relied on the exec command to run the build, but using it together with the restart event caused the build to execute twice on every change. To avoid that duplication, we leave exec as a dummy command (node -e "") that satisfies Nodemon's requirement for an executable process, while the actual build is now triggered only through the restart event.
At first, I considered using Nodemon's start event to handle the initial build, but it turned out to behave just like restart, except it does not provide the FILENAME environment variable.
Since the project needs the build to run at least once before file monitoring begins, I chained the build and Nodemon commands in the watch script.
The rest of the configuration remains mostly unchanged. In the GitHub version, I simply replaced the ignore pattern *.min.* with bin/, which is equivalent since all and only minified files are stored within bin folders.
You can now launch the entire workflow with the watch script that initially builds the whole project, then continuously monitors the project and triggers template rebuilds only when necessary.
4. Use Nodemon API
Everything that can be done with Nodemon from the command line can also be achieved programmatically using its JavaScript API.
To illustrate this, we can create a general-purpose script called watch.js that takes three arguments:
- the root directory to watch,
- an ignore list, and
- the script to execute when a change occurs.
This approach mirrors the fs.watch script from the previous article, except that here we use Nodemon as the watcher:
watch.js
View on GitHub
import { execSync } from "child_process";
import { argv } from "process";
import nodemon from "nodemon";
const { root, ignoreList, script } = parseArgv(argv.slice(2));
const options = { watch: root, ignore: ignoreList };
nodemon(options).on("restart", execCommand);
nodemon.emit("restart");
function execCommand(watchedFile) {
let env = {};
if (watchedFile) env = { FILENAME: watchedFile[0] };
execSync(`node "${script}"`, { stdio: "inherit", env });
}
...
The options object here is very similar to what you would normally define in the nodemonConfig section of the package.json file. Anything configurable through the CLI or JSON configuration can also be passed directly to nodemon() as options. This makes it possible to maintain a general configuration in package.json while applying specific runtime overrides in the watch.js script, such as project-specific watch paths or ignore lists:
package.json
View on GitHub
{
"nodemonConfig": {
"exec": "node -e \"\"",
"ext": "*"
},
...
}
A key difference between defining an event handler in the JSON configuration and using the API directly is that you cannot programmatically trigger a restart event if the callback is defined in the configuration. By attaching the handler through nodemon.on(), you gain control over when and how the restart logic executes, including the ability to manually emit the event with nodemon.emit().
In this example, the listener receives the modified file as the only element of an array. That path is stored in an environment variable named FILENAME and passed to the build.js process. When the watcher starts for the first time, no file has been changed yet, so the watchedFile argument is undefined. In that case, the manual call to nodemon.emit() ensures the initial build runs once before monitoring begins, mirroring the behavior established in the previous section.
In this section, the workflow is slightly different from the previous setup. Previously, the Rollup configuration imported the build.js script to ensure that all templates were preprocessed before bundling began. This time, the order is reversed, and the build script itself now calls the bundler directly:
build.js
View on GitHub
import minifyTemplates from "./auto/minify.js";
import { env } from "process";
await minifyTemplates([
env.FILENAME ??
getAllTemplates()
].flat());
bundleJS();
...
The package.json scripts are updated as follows:
package.json
View on GitHub
{
"scripts": {
"build": "node build",
"watch": "node watch --root=src --ignore=*.min.* build"
},
...
}
5. Conclusion
One limitation became clear during testing: even when adjusting the restart delay, Nodemon's restart callback does not expose a batch of modified files. It only reports the last file that triggered the restart. This was disappointing, as it prevents more advanced rebuild optimizations where multiple file changes could be processed together. I demonstrated an example of batch file detection in the previous post, using the File System API to handle grouped updates efficiently.
Despite this limitation, Nodemon remains a remarkably practical tool for automating builds and reloads in Node.js projects. While Node's native watch mode and the File System API have caught up in recent versions, Nodemon still stands out for its simplicity.
Comments
Post a Comment