5 min read
Building a FileWatcher in Node.js

Starting with Node.js may be confusing at first.

There are just too many things happening under the hood, it makes it a very dense forest for us, especially at the beginning.

Node works with modules.

Each module is a collection of classes, functions and utilities grouped by purpose.

Whenever you want to monitor, write, read or push something into the HTTP protocol – Node will be your butler of choice.

What we are going to build

Learning first principles means: Doing things the raw way to really understand how it works.

Things don’t have to be perfect, but you’ll bring them to life, test them, and get a proper feel for how they work. Therefore I built a FileWatcher using two built-in Node.js modules:

EventEmitter – a class that lets objects emit named events and react to them via listeners.

fs – the file system module, used here to watch a directory for changes and read its contents.

Read more about them in the Node.js docs: Events and File System.

The FileWatcher should monitor a folder and whenever a file is added or deleted or moved, it should print a message to the console.

Let’s have a look at the snippet and strip it down to its parts to check what is going on there:

import EventEmitter from 'node:events';
import fs from 'node:fs';

class FileWatcher extends EventEmitter {
	constructor(dir) {
		super();
		this.dir = dir;
	}

	watch() {
		fs.watch(this.dir, () => {
			const count = fs.readdirSync(this.dir).length;
			this.emit('change', count);
		});
	}
}

const watcher = new FileWatcher('./monitored');

watcher.on('change', (count) => {
	console.log(`Files in folder: ${count}`);
});

watcher.watch();

What’s actually happening here?

Extending EventEmitter

class FileWatcher extends EventEmitter {}

FileWatcher is a subclass of EventEmitter. Check the Node Docs here

By extending it, FileWatcher inherits all of its methods and properties without copying anything.

The most important ones:

MethodWhat it does
.on(event, fn)Registers a listener – runs fn every time event fires
.emit(event, ...args)Fires a named event and passes data to all listeners
.off(event, fn)Removes a specific listener
.once(event, fn)Like .on() but the listener runs only once, then removes itself

They are resolved at runtime via the prototype chain – the mechanism JavaScript uses to look up methods on parent classes. MDN: Prototype chain

The constructor

constructor(dir) {
	super();
	this.dir = dir;
}

dir is just a parameter name – it could be anything. It holds the path to the folder we want to watch.

super() is required whenever you write a constructor in a subclass. It calls the constructor of the parent class – here EventEmitter – so that this is properly initialized. Without it, Node throws an error.

You can read about it here.

this.dir = dir attaches the path to the instance so the other methods can access it.

The watch method

watch() {
	fs.watch(this.dir, () => {
		const count = fs.readdirSync(this.dir).length;
		this.emit('change', count);
	});
}

fs is Node’s built-in file system module. It gives you methods to read, write, and monitor files and directories.

fs.watch() listens to a directory for any changes — files added, deleted or renamed. When something changes, the callback fires.

fs.readdirSync() reads the directory and returns an array of filenames — ['a.txt', 'b.txt', 'c.txt']. .length gives us the count. The Sync suffix means it blocks execution until the read is done. The async alternative is fs.readdir(), which takes a callback instead and doesn’t block — relevant once things get more complex.

this.emit('change', count) is where the event system kicks in. emit fires a named event — here 'change' — and passes data along with it, here the current file count. Any listener registered with .on('change', ...) will now run and receive that count. The string 'change' is the channel — emit sends on it, on listens to it.

Instantiating and listening

const watcher = new FileWatcher('./monitored');
watcher.on('change', (count) => {
	console.log(`Files in folder: ${count}`);
});
watcher.watch();

new FileWatcher('./monitored') creates an instance and passes the folder path into the constructor.

.on('change', ...) is the receiving end. It waits for emit('change', ...) to fire and then runs the callback with whatever data was passed — here the file count.

.watch() starts the whole thing.

Conclusion

That’s it. Just two built-in modules wired together. The pattern you see here, extending a base class to get event capabilities, shows up everywhere in Node.js. Once you recognise it, you’ll spot it in http.Server, fs.ReadStream, and most stream-based APIs.

Note: fs.watch() can behave inconsistently across operating systems – firing multiple events for a single change. For production use, chokidar is the go-to alternative.