JavaScript Events: Node.js Events in Multiple Files

JavaScript Events: Learn how to use Node.js events across multiple files in a way that is scalable and usable

Share This Post

We already know all the power of running async code with JavaScript. In fact, it allows us to wait for something to complete, such as a call to an external API. Yet, there are times when this approach is not ideal. The classic example of this is a piece of code that takes a lot of time to run. For example, sending an email via SMTP may take several seconds, and we do not want to leave the user to hang waiting. Instead, Node.js events offer us a more elegant alternative. In this tutorial we will see how to use them, and how to structure a proper design pattern across multiple files.

This tutorial is about events in Node.js, so we are not talking about events in the browser. If you want to learn more about those, head to this article about events in the browser.

Understanding Events

Working with events is a lot like working with async code. In fact, in the end is another and more elegant way to do exactly that.

To make a more tangible parallelism, imagine you have to order something online. You will add it to your cart, pay, and you will end up on a page that says “Transaction completed”. Yet, the work the e-commerce site has to do is far from complete: it is just starting. They will have to look for the item you requested in the stock, package it, label it with the shipping address, and it over to the shipping company and eventually deliver it to you. Of course, you don’t want the page to keep loading until they actually deliver the package to you. You just want to know you have triggered their internal processes that will result in the shipment.

Events are exactly the same in JavaScript and Node.js. You have one part of your code that makes a request, or rather that says that something must be done. Then, another part of your code will take that work and do it at a later stage. The first part will just trust that as soon as they come up with the request, some other part of the code will take care of it eventually.

The part of the code that requests something to be done, is emitting the event. Instead, the part of the code that waits for those events to process them is a subscriber, or listener.

Working with Multiple Events

We can have many events running on our platform at any given time. Even if they are not happening all at the same time, there is the possibility that they do, and you want to deal with each separetely.

Thus, each event must have its own name. This is important because in this way we can have a different subscriber for each event (the subscriber to process orders won’t be the same as the one we use to send emails). Beyond that, each event has also a payload: some data the subscriber needs to do the work. In the email example, the subscriber will need to know what mail to actually send.

Therefore, when we emit (or fire it) an event we will do it by providing its event name, and its payload. On the other side, when we listen to an event we will need to provide the name of the event we are listening to, and a function that can process its payload.

Implementing Events in Node.js

Now that we have know some theory we can dive into the actual implementation. We will start from the basics, and then move on to a bigger design across multiple files.

The Basics

Working with events is easy, we only need to have an EventEmitter. This special object is the one handling the firing and listening of events. It will take care of receiving events from emitters, and landing them into subscribers’ hands. We can find the EventEmitter inside the events package, which already comes with Node.js (no installation required).

First, we need to instantiate the event emitter (new EventEmitter()).

We have to provide some listeners to the event, and we can do that with the on() function. This wants the name of the event to listen to as first parameter and a function as a second parameter. That is the function we will call when the event gets emitted. Hence, that function accepts the event payload as parameter, and can do something with it.

Once we address that, we can emit events by providing a name and payload with the emit() function.

import { EventEmitter } from 'events';

const emitter = new EventEmitter();

emitter.on('send_mail', (payload) => {
  console.log(payload);
});

emitter.emit('send_email', 'Test email');

We can also wire it in a more expres.js-friendly way, by emitting the event inside a route.

import { EventEmitter } from 'events';
import express from 'express';

const emitter = new EventEmitter();
const app = express();

emitter.on('send_mail', (payload) => {
  console.log(payload);
});

app.get('/emit', (req, res, next) => {
  emitter.emit('send_email', 'Test email');
  res.send('Email has been queued');
  next();
});

Multiple Event Emitters

As you see in the previous example, we need to create an EventEmitter and then listen and emit on it. However, we can even use multiple event emitters if we want. This, of course, would be a complex setup that in most cases you don’t really need, but it helps to understand some caveats.

const em1 = new EventEmitter();
const em2 = new EventEmitter();

If we add a listener with on() on em1, only events that we emit on em1 with em1.emit() will be passed to that listener. In fact, em2 will be a completely different event emitter, that is totally separate and that does not communicate with em1.

This may seem obvious at first, but it has some nasty implications. What happens when we work with multiple files, pheraps exporting an event emitter? Every time that we import it, are we importing the same event emitter, or are we creating every time a new one?

To answer these questions, we have the next chapter.

Node.js Events in Multiple Files

This part of the tutorial is specifically designed for Express.js developers. In fact, in a large express application, you want to have your routes and services in separate files, and the same goes for your subscribers. Hence, we want to use the same event emitter across multiple files. After an afternoon of trial end error, here are my findings.

The structure of your Node.js project is key to arrange events across multiple files. Here, we are following the Bulletproof node.js project architecture, by Sam Quinn. I will never be able to recommend it enough! Any Node.js project should implement this structure because it is stable and scalable. So, be sure to check that out before continuing to read.

What do we need to deal with events across multiple files? Mainly four things:

  1. One Event Emitter in a dedicated file, that contains only that
  2. One loader, that will take care of loading the /subscribers folder and all the listeners defined there
  3. An index file inside our /subscribers folder, that will take care of attaching all the listeners to an emitter
  4. A function for each event we want to listen to, to handle its payload

Now that we have a macro-structure on how to proceed, we can dive into implementation. Since each item requires the next to work, we will start from the bottom-up instead.

Listeners Functions

Create a /src/subscribers folder if you haven’t already. Then, you can create a JavaScript file for each event you want to manage. My recommendation is to keep the file name identical to the event you want to listen to. It’s not required, but it will definitely help by bringing some clarity.

A common naming convention for events is to have imperative tense with snake case (underscores). For example: do_something, send_email, process_order, trigger_security_event. Thus, our files will be like do_something.js and so on.

Inside each of those files, we only need to export by default a single function. That is the function that will take care of processing the event, and handle its payload. For example, in log_debug.js we might have:

export default function(payload) {
  console.log(payload);
};

A Function Returning a Function

This is a pro tip that will save make your day at some point. In fact, it will help you have more good days than note when it comes to developing with Node.js.

In the previous code snippet, we exported directly the function that will handle the event. Instead of doing so, we should export another function that will generate the listener when called. In this way, we can provide some parameters to it and generate a different listener every time, based on its parameter. That’s dependency injection.

export default makeListener({ database } = {}) {
  const db = database || new Database();
  // The actual listener
  return async (payload) => {
    await db.save(payload);
    console.log('Saved');
  };
};

Obviously, in this example the database class is fake, but it helps you understand how dependency injection works. In this case, when we want to attach the listener we can do the following.

const listener = makeListener();
emitter.on('do_something', listener);

// More compact alternative, note the brackets after makeListener
emitter.on('do_something', makeListener());

This is the reccomended approach, and the one that we will be following.

An index to Load All the Listeners

Now, we can create /src/subscribers/index.js. In there, we will export as default just one function, that takes an Event Emitter as parameter. This function will apply all the listeners in /src/subscribers to the emitter provided as parameter, and eventually, return it. In this way, we are defining what events to attach in a way that is independent of the actual listener.

import doSomething from './do_something';
// Import other listeners

export default function loadListeners(emitter) {
  emitter.on('do_something', doSomething());
  // Attach other events
  return emitter;
};

As simple as that, we now have once simple function that can prepare any emitter that we give to it. We can put that to use, but before that we need to create our global event emitter.

A Global Event Emitter

As you may recall from earlier in this tutorial, each event emitter is independent. Hence, if we want to be sure that we use always the same one to listen and to emit events.

We can do that by creating a new one as a constant and then exporting it in a dedicated file. As such, create the file /src/loaders/eventEmitter.js. The content of this file is straightforward.

import { EventEmitter } from 'events';

const emitter = new EventEmitter();

export default emitter;

Now, whenever we are in our application we can import it and emit events on it.

import globalEmitter from './eventEmitter';

globalEmitter.emit('do_something', 'Message');

A Loader to Wire It All Together

Now we know how to emit events on our global emitter, but nothing will happen. That’s because we still need to trigger our loadListeners() function onto that emitter.

To do that, we can create a new loader named /src/loaders/events.js. Remember from our project structure, a loader is a function that takes the express.js app and prepares it to run before it actually runs. So, our loader will take the app but do nothing on it. Instead, it will simply attach our listeners to the global emitter.

We want to have this as a loader so that we are sure we start listening to all events before the application starts to run.

import emitter from './eventEmitter';
import loadListenersfrom '../subscribers';

export default ({ app }) => {
  loadListeners(emitter);
  return app;
};

Obviously, we need to call this loader, and this is generally done in /src/loaders/index.js, but if you still have doubts the Bulletproof architecture will give you more details.

Node.js Events for Full Stack Developer

Node.js events are something any full-stack developer will need at some point. Fortunately, this tutorial is part of a broader guide to help you become a Full Stack Developer yourself (oh, and it’s completely free).

As part of this guide, we try to put into practice everything that we learn. That’s the only way concepts will stick into your mind. This tutorial is no exception, and we will continue to work on our pretend bakery store website that you can check on GitHub.com at alessandromaggio/full-stack-course.

In fact, if you haven’t already, you should download the latest commit from here and start working on it, by following this assignment.

The Assignment

For this assignment, we need to implement the multiple file events structure in the /server folder. Then, we need to implement just one simple event handler.

This event handler, or listener, will take care of logging messages. The payload of the event, however, shouldn’t be a string, but instead an object containing two properties: time and message. As you can imagine, the time is the timestamp of when the message was generated, and the message is a descriptive string. You can name the event log_message.

This is important, as events may be processed asynchronously and not in the order they are generated. Definitely not at the precise time they are generated, so the time must be gathered when originating/emitting the event, and not when processing it.

For now, our listener will log to the console. So, why not logging directly to the console? At some point in the future, we may switch to log onto an external system, but this is time-consuming and we don’t want to slow-down user’s operations just for some logs.

Try implementing this on your own before checking the solution below. Even if you are stuck, try harder a couple of times before checking the solution.

The more the task is challenging, the more you are learning.

The Solution

Since you are checking the solution now, I assume you tried the best you can and hopefully succeeded. If you think you can try even harder, then do that before reading the explanation.

To check the full code of this tutorial, you should head to GitHub.om and specifically take a look at this commit.

To use import statements (instead of require), we had to install some babel dependencies:

npm install --save @babel/core @babel/node @babel/preset-env

Then, we had to create a .babelrc file to provide configuration to babel, at the root of /server, in the same place where we have our index.js. The content of this file is the following.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": "3.0.0"
      }
    ]
  ]
}
  

Now that we took care of the basics, here are the files that we created:

  • /loaders/eventEmitter.js contains the event emitter
  • /loaders/events.js takes the listeners from the /subscribers folder and attach it to the emitter
  • /loaders/index.js runs all the listeners in an async function. At the moment, obviously, we only have the events loader which is not even async, but who knows what we might add in the future
  • /subscribers/log_message.js takes care of handling the event after it is fired
  • /subscribers/index.js attaches all the listeners to an event emitter

This allows us to create the /subscribers/log_message.js with the following content.

export default function logMessage() {
  return ({ time, message }) => {
    console.log(`${time}\t${message}`);
  }
};

As simple as that. Now, we can finally wire the event in our index.js. Note that we also took the opportunity to move all the code from all over the file into a more ordered startServer() function.

import express from 'express';
import loaders from './loaders';
import globalEmitter from './loaders/eventEmitter';

const port = 3000;

async function startServer() {
  const app = express();
  await loaders({ app });

  app.use(express.json());

  app.get('/alive', (req, res, next) => {
    res.send('OK');
    globalEmitter.emit('log_message', { time: Date.now(), message: 'A message' })
    next();
  });

  app.use((req, res, next) => {
    console.log(`${req.method} ${req.url} ${res.statusCode}`);
    next();
  });

  app.listen(port, () => {
    console.log('Started');
  });
}

startServer();

To run this code, we now need babel (because we have import statements). So, the code to run it is this:

babel-node index.js

If we install nodemon as a global dependency (npm install -g nodemon), we can even let it run and refresh automatically whenever we change a file. To do that, we will have to run the following command.

nodemon --exec babel-node index.js

To Wrap Up

In this tutorial we saw much more than just how to use events with Javascript. We saw how to use them properly, and how to integrate them in a proper project architecture.

This is a valuable tool you have as a developer, even if you might not realize it now. It is crucial when building microservices, and more in general applications that scale to support millions of users. So good luck with your development endeavors!

Picture of Alessandro Maggio

Alessandro Maggio

Project manager, critical-thinker, passionate about networking & coding. I believe that time is the most precious resource we have, and that technology can help us not to waste it. I founded ICTShore.com with the same principle: I share what I learn so that you get value from it faster than I did.
Picture of Alessandro Maggio

Alessandro Maggio

Project manager, critical-thinker, passionate about networking & coding. I believe that time is the most precious resource we have, and that technology can help us not to waste it. I founded ICTShore.com with the same principle: I share what I learn so that you get value from it faster than I did.

Alessandro Maggio

2021-05-10T16:30:01+00:00

Unspecified

Full Stack Development Course

Unspecified