Node.js And Its Event-Driven Architecture

Uriel Rodriguez
4 min readFeb 28, 2021

Lately, as I have been focused on Node.js development, I have spent time learning about the underlying ideas that power the platform. Node.js departs from more traditional methods of handling I/O operations via the usage of threads, accomplishing efficiency and speed with its non-blocking I/O design. This non-blocking design is made possible through asynchronous handling of operations, by utilizing what’s known as an event-driven model. What this means is that at the core, Node.js uses events to set processes in motion and executes and completes them only when they are ready.

An event can be understood as some sort of action occurring, and another piece of code responding to the action. Node.js implements this idea through a special object known as the EventEmitter. The EventEmitter object is at the core of the event-driven architecture and lends other event-based implementations the ability to create and respond to events.

// reminder.mjs - .mjs file extension used to implement ES Modulesimport EventEmitter from 'events';class Reminder extends EventEmitter {
constructor(time) {
super();
this.time = time;
}
remind() {
setInterval(() => {
this.emit('reminder');
}, this.time);
}
}
const minToMilliSecs = min => 60,000 * min;const breakReminder = new Reminder(minToMilliSecs(20));
breakReminder.on('reminder', () => console.log('take a break'));
breakReminder.remind();

In this example, we can see how Node.js utilizes the EventEmitter object, mainly how it provides other objects the ability to create and respond to events. First, the EventEmitter object is imported from the core modules, “events”. After this, an object can inherit the ability to create events via the emit() method and respond to events via the on() method. Here I created a Reminder class that gives instances the ability to create reminder events utilizing the time provided when creating the reminder instance. After 20 minutes have passed, the Reminder instance, “breakReminder”, is set to disperse or emit an event, the “reminder” event. This is accomplished via the emit() method which takes an event name as its first argument and can be set to anything. In addition, the break reminder is also configured to listen to the “reminder” event and to log to the console via the on() method which takes the event name it should respond to, and an event handler to perform some action on that event.

So how does this event-driven process contribute to the speed and efficiency of Node.js? The process created by the setInterval function, emitting after some time, is ongoing and so it is handled in the background by a separate structure, allowing the application to resume execution. In this way, operations that require time to process, such as I/O operations, like accessing data from a database, can be handled while preventing the application from stopping due to I/O processing time. Events are then used to inform that some time-consuming process has completed, and is ready for the application to process its data. This idea is implemented in web servers via Node.js’ HTTPServer and HTTPClient objects.

Aside from being foundational to web applications, the HTTPServer object help to further understand the event-driven architecture in Node.js. As an event emitter, the HTTPServer object listens for events, a “request” from the client, and on that event, emits its own event, the “response” which the client listens for and processes.

// server.mjsimport * as http from 'http';const server = http.createServer();server.on('request', (req, res) => {
res.writeHead(...);
res.end(...server response);
});
server.listen(port, someURL);

This example further demonstrates the idea behind events and their implementations in Node.js with servers. Here we are instantiating a server object that is configured to handle an event, “request”, created by the client, which is also an event emitter. The server object is configured to listen for a specific client and its request via the listen() and the specified URL. Once the specified client emits the “request” event, the server can then emit its own event, the “response” which the client will listen for. Something to note about this example is that the lower level implementations expressed in the Reminder class example, where the emit() method is used to send out an event, are abstracted away within the HTTP module, but the functionality remains the same.

At this point, we can understand the idea of events and handlers but what goes on behind the scenes and what we don’t see is very important. I/O operations, like making requests to a server, are handled asynchronously and events are the mechanism used to determine when the process is ready to return some value or output. Any asynchronous operation is outsourced, to other structures within the platform, allowing the script to continue execution thus avoiding blocking. When outsourced, the asynchronous process continues running until it emits an event that signals its completion. Once this is the case, the event and the handler function provided with the asynchronous process, the callback, are transferred over to an event queue. At this point, the event loop iterates over the events, which represent completed asynchronous processes, and pushes the event handler or callback to the call stack where it can be finally executed.

Despite the complexities of the internal structures that handle the asynchronous processes and events, the central idea remains: events are key to allowing fast and efficient processing. By understanding this, applications can be implemented in ways that take advantage of the structures that handle time-consuming tasks and avoid blocking the server. For example, carrying out computationally taxing tasks, such as large mathematical computations, especially in a synchronous manner could block the server and prevent users from carrying out other tasks. This is where an event-driven approach would aid to distribute the computation to operate in the background while the application is free to carry out further processes. Learning how Node.js operates behind the scenes is important for efficient implementations. To learn more, check out Node.js official documentation on events.

--

--

Uriel Rodriguez

Flatiron School alumni and Full Stack web developer.