4 APPROACHES TO LINKING MODULES IN NODE.JS
7 min
Nov 04 2020
Many Node.js developers exclusively use require() to link modules, but there are other approaches with their pros and cons. We’ll tell you about them in this article. We will consider four approaches:

About the modules

Modules and modular architecture are the foundation of Node.js. Modules provide encapsulation (hiding implementation details and only exposing the interface with module.exports), code reuse, logical splitting of code into files. Almost all Node.js applications contain many modules that must interact in some way. Suppose you link modules incorrectly or even let the interaction of modules go by chance. In that case, you can find that the application begins to "fall apart": code changes in one place lead to a breakdown in another, and unit testing becomes merely impossible. Ideally, modules should have high cohesion but low coupling.

Hard dependencies

The hard dependence of one module on another arises when using require (). This is an effective, simple, and common approach. For example, we just want to connect the module responsible for interaction with the database:
`// ourModule.js
const db = require('db');
// `

Pros:

Cons:

Summary:

This approach is good for small applications or prototypes and for connecting stateless modules: factories, constructors, and functions.

Dependency Injection

The main idea behind dependency injection is to pass dependencies from an external component to a module. Thus, the hard dependency in the module is removed, and it becomes possible to reuse it in different contexts (for example, with varying instances of a database). Dependency injection can be done by passing the dependency in the constructor argument or by setting module properties, but in practice, it is better to use the first method. Let's put dependency injection into practice by creating a database instance using a factory and passing it to our module:
`// ourModule.js
module.exports = (db) => {
// Module initialization with passed database instance...	
};`
External module:
`const dbFactory = require('db');
const OurModule = require('./ourModule.js');
const dbInstance = dbFactory.createInstance('instance1');
const ourModule = OurModule(dbInstance);`

Now we can reuse our module and easily write a unit test for it: just create a mock object of a database instance and pass it to the module.

Pros:

Cons:

More careful design of dependencies: a certain order of initialization of modules must be observed The complexity of managing dependencies, especially when there are many Module code becomes less readable we can not directly look at that dependency when it comes from outside

Summary:

Dependency Injection increases the application’s complexity and size, but in return, provides reusability and ease of testing. The developer should decide what is more critical for him in a particular case - the simplicity of a hard dependency or more extensive dependency injection capabilities.

Service Locator

The idea is to have a dependency registry that acts as a mediator for any module to load a dependency. Instead of hard linking, the dependencies are requested by the module from the service locator. Modules have a new dependency - the service locator itself. An example of a service locator is the Node.js module system: modules request a dependency using require(). In the next example, we will create a service locator, register instances of the database, and our module.
`// serviceLocator.js
const dependencies = {};
const factories = {};
const serviceLocator = {};
serviceLocator.register = (name, instance) => { //2
  dependenciesname = instance;
};
serviceLocator.factory = (name, factory) => { //1
  factoriesname = factory;
};
serviceLocator.get = (name) => { //3
  if(!dependenciesname) {
    const factory = factoriesname;
    dependenciesname = factory && factory(serviceLocator);
    if(!dependenciesname) {
      throw new Error('Cannot find module: ' + name);
    }
  }
  return dependenciesname;
};</pre><div class="text">External module:</div><pre>const serviceLocator = require('./serviceLocator.js')();
serviceLocator.register('someParameter', 'someValue');
serviceLocator.factory('db', require('db'));
serviceLocator.factory('ourModule', require('ourModule'));
const ourModule = serviceLocator.get('ourModule');</pre><div class="text">Our module:</div><pre>// ourModule.js
module.exports = (serviceLocator) => {
  const db = serviceLocator.get('db');
  const someValue = serviceLocator.get('someParameter');
  const ourModule = {};
  // Module initialization, work with the database ...
  return ourModule;
};`
It should be noted that the service locator stores service factories instead of instances, which makes sense. We got the advantages of "lazy" initialization. Moreover, now we don't have to worry about initializing modules - all modules will be initialized when needed. Plus, we got the ability to store parameters in the service locator.

Pros:

Cons:

Module reuse is harder than dependency injection (due to additional service locator dependency) Readability: It's even harder to understand what the dependency required of the service locator does Increased coupling in comparison with dependency injection

Summary

In general, a service locator is similar to dependency injection, in some ways it is easier (no initialization order), in others, it is more complicated (less possibility of code reuse).

DI Container

The service locator has a drawback; it is rarely used in practice - the dependence of modules on the locator itself. Dependency injection containers do not have this drawback. This is essentially the same service locator with an additional function that defines the module's dependencies before instantiating it. You can determine module dependencies by parsing and extracting arguments from the module's constructor (in JavaScript, you can cast a function reference to a string using toString()). This method is suitable if the development is purely for the server. If the client code is written, it is often minified, and it will be pointless to extract the arguments. In this case, the list of dependencies can be presented as an array of strings (this approach is used in Angular.js, based on the use of DI containers). Let's implement a DI container using parsing of constructor arguments:
`const fnArgs = require('parse-fn-args');
module.exports = function() {
  const dependencies = {};
  const factories = {}; 
  const diContainer = {};
  diContainer.factory = (name, factory) => {
    factoriesname = factory;
  };
  diContainer.register = (name, dep) => {
    dependenciesname = dep;
  };
  diContainer.get = (name) => {
    if(!dependenciesname) {
      const factory = factoriesname;
      dependenciesname = factory && diContainer.inject(factory);
    if(!dependenciesname) {
      throw new Error('Cannot find module: ' + name);
    }
  }
  diContainer.inject = (factory) => {
    const args = fnArgs(factory)
      .map(dependency => diContainer.get(dependency));
    return factory.apply(null, args);
  }
  return dependenciesname;
};`
In comparison to the service locator, an inject method that determines the module's dependencies before instantiating it was added. The external module code has hardly changed:
`const diContainer = require('./diContainer.js')();
diContainer.register('someParameter', 'someValue');
diContainer.factory('db', require('db'));
diContainer.factory('ourModule', require('ourModule'));
const ourModule = diContainer.get('ourModule');`
Our module looks exactly the same as with simple dependency injection:
`// ourModule.js
module.exports = (db) => {
// Module initialization with passed database instance...		
};`
Our module can now be called both using a DI container or by passing all required instances of dependencies directly using simple dependency injection

Pros:

The biggest con:

Significant complication of the module linking logic

Summary

This approach is more difficult to understand and contains a little more code, but it is well worth your time because of its power and elegance. This approach may be overkill in small projects but should be considered if you are building an extensive application.

Conclusion

We covered the basic approaches to linking modules in Node.js. As is usually the case, there is no silver bullet, but the developer should be aware of the possible alternatives and choose the most appropriate solution for each specific case.
BLOG LATEST
Popular articles
GOT AN IDEA? LET'S DISCUSS!
Share your project’s scope, timeline, technical requirements, business challenges, and other details you consider necessary. Our team will study them and contact you soon. Let’s make an exciting product together!
By sending this form I confirm that I have read and accept the Privacy Policy
WELCOME TO OUR OFFICES
Georgia
Russia