Chapter 5. Wiring Modules

The Node.js module system brilliantly fills an old gap in the JavaScript language: the lack of a native way of organizing code into different self-contained units. One of its biggest advantages is the ability to link these modules together using the require() function (as we have seen in Chapter 1, Node.js Design Fundamentals), a simple yet powerful approach. However, many developers new to Node.js might find this confusing; one of the most asked questions is in fact: what's the best way to pass an instance of component X into module Y?

Sometimes, this confusion results in a desperate quest for the Singleton pattern in the hope of finding a more familiar way to link our modules together. On the other side, some might overuse the Dependency Injection pattern, leveraging it to handle any type of dependency (even stateless) without a particular reason. It should not be surprising that the art of module wiring is one of the most controversial and opinionated topics in Node.js. There are many schools of thought influencing this area, but none of them can be considered to possess the undisputed truth. Every approach, in fact, has its pros and cons and they often end up mixed together in the same application, adapted, customized, or used in disguise under other names.

In this chapter, we're going to analyze the various approaches for wiring modules and highlight their strengths and weaknesses, so that we can rationally choose and mix them together depending on the balance between simplicity, reusability, and extensibility that we want to obtain. In particular, we're going to present the most important patterns related to this topic, which are as follows:

We will then explore a closely related problem, namely, how to wire plugins. This can be considered a specialization of module wiring and it mostly presents the same traits, but the context of its application is slightly different and presents its own challenges, especially when a plugin is distributed as a separate Node.js package. We will learn the main techniques to create a plugin-capable architecture and we will then focus on how to integrate these plugins into the flow of the main application.

At the end of this chapter, the obscure art of Node.js module wiring should not be a mystery to us anymore.

Every modern application is the result of the aggregation of several components and, as the application grows, the way we connect these components becomes a win or lose factor. It's not only a problem related to technical aspects such as extensibility, but it's also a concern with the way we perceive the system. A tangled dependency graph is a liability and it adds to the technical debt of the project; in such a situation, any change in the code aimed to either modify or extend its functionality can result in a tremendous effort.

In the worst case, the components are so tightly connected together that it becomes impossible to add or change anything without refactoring or even completely rewriting entire parts of the application. This, of course, does not mean that we have to over-engineer our design starting from the very first module, but surely finding a good balance from the very beginning can make a huge difference.

Node.js provides a great tool for organizing and wiring the components of an application together: it's the CommonJS module system. However, the module system alone is not a guarantee for success; if on one side, it adds a convenient level of indirection between the client module and the dependency, then on the other, it might introduce a tighter coupling if not used properly. In this section, we will discuss some fundamental aspects of dependency wiring in Node.js.

In software architecture, we can consider any entity, state, or data format, which influences the behavior or structure of a component, as a dependency. For example, a component might use the services offered by another component, rely on a particular global state of the system, or implement a specific communication protocol in order to exchange information with other components, and so on. The concept of dependency is very broad and sometimes hard to evaluate.

In Node.js, though, we can immediately identify one essential type of dependency, which is the most common and easy to identify; of course, we are talking about the dependency between modules. Modules are the fundamental mechanism at our disposal to organize and structure our code; it's unreasonable to build a large application without relying on the module system at all. If used properly to group the various elements of an application, it can bring a lot of advantages. In fact, the properties of a module can be summed up as follows:

A module represents the perfect level of granularity for performing information hiding and gives an effective mechanism to expose only the public interface of a component (using module.exports).

However, simply spreading the functionality of an application or a library across different modules is not enough for a successful design; it has to be done right. One of the fallacies is ending up in a situation where the relationship between our modules becomes so strong we create a unique monolithic entity, where removing or replacing a module would reverberate across most of the architecture. We are immediately able to recognize that the way we organize our code into modules and the way we connect them together, play a strategic role. And as with any problem in software design, it's a matter of finding the right balance between different measures.

In JavaScript, everything is an object. We don't have abstract concepts such as pure interfaces or classes; its dynamic typing already provides a natural mechanism to decouple the interface (or policy) from the implementation (or detail). That's one of the reasons why some of the design patterns that we have seen in Chapter 4, Design Patterns, looked so different and simplified compared to their traditional implementation.

In JavaScript, we have minimal problems in separating interfaces from implementations; however, by simply using the Node.js module system, we are already introducing a hardcoded relationship with one particular implementation. Under normal conditions, there is nothing wrong with this, but if we use require() to load a module that exports a stateful instance, such as a db handle, an HTTP server instance, the instance of a service, or in general any object which is not stateless, we are actually referencing something very similar to a Singleton, thus inheriting its pros and cons, with the addition of some caveats.

A lot of people new to Node.js get confused about how to implement the Singleton pattern correctly, most of the time with the simple intent of sharing an instance across the various modules of an application. But, the answer in Node.js is easier than what we might think; simply exporting an instance using module.exports is already enough to obtain something very similar to the Singleton pattern. Consider, for example, the following line of code:

By simply exporting a new instance of our database, we can already assume that within the current package (which can easily be the entire code of our application), we are going to have only one instance of the db module. This is possible because, as we know, Node.js will cache the module after the first invocation of require(), making sure to not execute it again at any subsequent invocation, returning instead the cached instance. For example, we can easily obtain a shared instance of the db module that we defined earlier, with the following line of code:

But there is a caveat; the module is cached using its full path as lookup key, therefore it is guaranteed to be a Singleton only within the current package. We saw in Chapter 1, Node.js Design Fundamentals, that each package might have its own set of private dependencies inside its node_modules directory, which might result in multiple instances of the same package and therefore of the same module, with the result that our Singleton might not be single anymore. Consider, for example, the case where the db module is wrapped into a package named mydb. The following lines of code will be in its package.json file:

{
  "name": "mydb",
  "main": "db.js"
}

Now consider the following package dependency tree:

app/
`-- node_modules
    |-- packageA
    |  `-- node_modules
    |      `-- mydb
    `-- packageB
        `-- node_modules
            `-- mydb

Both packageA and packageB have a dependency on the mydb package; in turn, the app package, which is our main application, depends on packageA and packageB. The scenario we just described, will break the assumption about the uniqueness of the database instance; in fact, both packageA and packageB will load the database instance using a command such as the following:

However, packageA and packageB will actually load two different instances of our pretending singleton, because the mydb module will resolve to a different directory depending on the package it is required from.

At this point, we can easily say that the Singleton pattern, as described in the literature, does not exist in Node.js unless we don't use a real global variable to store it, something such as the following:

This would guarantee that the instance will be only one and shared across the entire application, and not just the same package. However, this is a practice to avoid at all costs; most of the time, we don't really need a pure Singleton, and anyway, as we will see later, there are other patterns that we can use to share an instance across the different packages.