Node.js: using fs.watch to re-import CommonJS modules.

Gemma BlackGemma Black
3 min read

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 than fs.watchFile and fs.unwatchFile. fs.watch should be used instead of fs.watchFile and fs.unwatchFile when possible. - https://nodejs.org/docs/latest/api/fs.html#fswatchfilefilename-options-listener

Caveats

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.

  1. The main.js file has to be adjusted so that the child.js module is lazily-loaded when the root path is requested.

  2. 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 😉.

0
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.