Node.js: using fs.watch to re-import CommonJS modules.
Disclaimer. This is an investigation into what is possible. Not a fully-blown solution into how to reload modules without restarting the whole server.
In a previous article, I was investigating how to use require.cache
to refresh a module by deleting it and requiring it again. eg.
require('./some-module')
// use module
delete require.cache[require.resolve('./some-module')]
// use reloaded module
However, on its own, I can’t really think of a good use case to delete a cached module like that. My aim was to attempt to reload a module without restarting the server – mainly to reduce the wait time. And it works! To a degree.
fs.watchFile
So I have a child.js
module that doesn’t export anything.
// child.js
console.log('hi', Date.now())
I then have a main.js
that imports the child.js
.
const fs = require('fs')
require('./child')
fs.watchFile(require.resolve('./child.js'), () => {
console.log('refresh')
delete require.cache[require.resolve('./child')]
require('./child')
})
Running node main.js
with fs.watchFile
kept the server running on its own. So every time I made a change to the file, it would refresh the child module as I changed the logged output.
The only downside is that I noticed a little bit of lag when using fs.watchFile
. It didn’t pick up all the changes. So I tried fs.watch
.
Using fs.watch instead of fs.watchFile
The child.js
file was the same. The main.js
was the same with everything except fs.watchFile
was replaced with fs.watch
.
And the lag was gone. Changes were reflected instantaneously.
Now the reason for this is because fs.watchFile
uses polling. And the Node docs recommend using fs.watch
instead.
Using
fs.watch
()
is more efficient thanfs.watchFile
andfs.unwatchFile
.fs.watch
should be used instead offs.watchFile
andfs.unwatchFile
when possible. - https://nodejs.org/docs/latest/api/fs.html#fswatchfilefilename-options-listenerCaveats
The
fs.watch
API is not 100% consistent across platforms, and is unavailable in some situations.
An ExpressJS server
So now, how could I potentially get module reloading when a file changes without restarting the entire server?
First. Here’s a very basic Express.js server.
// -- child.js
module.exports = (req, res) => {
res.send('Hello, World!');
}
// -- main.js
const express = require('express');
const handle = require('./child');
const app = express();
app.get('/', handle);
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Now, when I run the server doing node main.js
, and make a request to the root path, http :3000
(using HTTPIE), I get my “Hello, World!” response.
But without Nodemon or some other way of watching the file, changes to the file won’t be reflected. So let’s change that.
The
main.js
file has to be adjusted so that thechild.js
module is lazily-loaded when the root path is requested.A file watcher was added to watch changes to the
child.js
.
const express = require('express');
const fs = require('fs');
fs.watch(require.resolve('./child.js'), () => {
delete require.cache[require.resolve('./child')]
require('./child')
})
const app = express();
app.get('/', (...args) => {
require('./child')(...args)
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
And voila 🎉!
I made a change to the file, it was reloaded without restarting the server!
And for development, this is fine. We can use a variable to have reloading in a development only environment running it like MODULE_RELOAD=true node main.js
.
const express = require('express');
const fs = require('fs');
const handle = require('./child');
const app = express();
if (process.env.MODULE_RELOAD) {
fs.watch(require.resolve('./child.js'), () => {
console.log('Server reloaded')
delete require.cache[require.resolve('./child.js')]
require('./child')
})
app.get('/', (...args) => {
require('./child')(...args)
});
} else {
app.get('/', handle);
}
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Of course, loading one file route path isn’t all that helpful either. But, I did say this was an investigation 😉.
Subscribe to my newsletter
Read articles from Gemma Black directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Gemma Black
Gemma Black
I'm a Senior Software Engineer. With 10+ years working within tech teams, and 20+ years working with code, I develop across the stack, assisting with application design, maintenance, deployment and DevOps within the AWS Cloud.