This page looks best with JavaScript enabled

Node.js: Module wiring and dependencies explained

 ·  ☕ 9 min read  ·  ✍️ Iskander Samatov

Node.js module



Any substantial software project consists of multiple files. In the older days of JavaScript making those files play nicely together and avoiding name collisions was one of the main sources of frustration.

Moduling system introduced with Node.js solved that major problem. The system is called CommonJs and thanks to it, component name collision is a thing of the past and each module has its own scope which is a safe haven for its components.

However, even with that problem solved, wiring modules still must be done with care. The system you setup for wiring your components together can make or break the project. While there’s no need for over-engineering from the get go, it certainly pays off to know how to properly wire your modules.

Cohesiveness and Coupling

Let’s first go over the basics.

Cohesiveness measures how well your module and its components are geared towards doing one thing well. For example, if you have a module dedicated solely to identifying and filtering spam messages, that module probably has high cohesiveness. On the other hand, if you have a module which exposes methods for modifying different kinds of database objects like user, account, post and etc., then that module will have low cohesiveness.

Coupling measures how much component is dependent on the other components. If the module directly accesses and modifies the data of the other module or some global variable, then that module is tightly coupled and will be harder to make changes to. If the only way for two modules to communicate is by exchanging values through their function parameters, then those modules are loosely coupled.

As a rule of thumb, in order to create scalable and maintainable system, you want to aim for high cohesiveness and loose coupling.

Stateful & Stateless modules

In Node.js modules can be stateless or stateful:

  • Stateful – a module that exposes an instance of some stateful object (db connection, third party service instance, socket and etc).
  • Stateless – a module that exposes stateless entities like classes or utility methods.

When working with stateful module you should have a Singleton pattern to make sure all the other modules that require the stateful module access the same instance. With Node.js you don’t have to worry about explicitly wrapping your stateful module in a Singleton pattern thanks to the mechanics of the CommonJs system.

The very first time you require your module, Node.js will cache that instance and on the subsequent requires will serve the same instance. The only time this would fail is if you require an instance that is outside of current package. The reason is because when caching the instance, Node.js uses that instance’s package address as a key.

It’s far more important to be careful when working with stateful modules than the stateless ones. While having tight coupling within the stateless modules can be bad for your architecture, the repercussions of poor wiring are far less damaging than the ones for the stateful instance.

Dependencies

The first thing that might come to mind when thinking about Node.js dependencies is the content of node_modules folder. However, any component or piece of data needed in order for your module to work correctly qualifies as a dependency. Dependency can be anything from database connection instance, to a simple string with the file path.

You might have heard terms “Hardcoded dependency” and “Dependency injection” and wondered about the difference. While it might sound a bit vague, the difference is pretty simple:

  • Hardcoded dependency refers to the module wiring pattern where you hardcode the name of the dependency inside of your module, possibly using require function.
  • Dependency injection is a wiring pattern where the dependencies are not hardcoded but provided as the input by the external entity instead.

To better illustrate the difference let’s build a simple stateful module for a movie suggestion using the two methods:

Hardcoded dependencies

Let’s start off with the module for exposing database connection:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const { Client } = require('pg');

const client = new Client({
  user: 'dbuser',
  host: 'database.server.com',
  database: 'mydb',
  password: 'secretpassword',
  port: 3211,
})
client.connect();

module.exports = client;

In this module we initiate a simple database connection instance and export it.

Next, our movie suggestion module where we will specify our database connection dependency by hardcoding it. For the brevity of the example I left out the implementation of the methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const client = require('./db');

exports.listSuggestions = (username, callback) => {
  client.query(`select * from suggestions where username = '${username}'`, (err, res) => {
    //...
  });
}


exports.newSuggestion = (username, callback) => {
  //...
}

Now let’s take a look at the app module which will act as the main entry point and start the server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const express = require('express');
const bodyParser = require('body-parser');
const {
  newSuggestion,
  listSuggestions,
} = require('./movies');

const app = express()
app.use(bodyParser.json())


app.get('/suggestion/new', (req, res) => {
  const { username } = req.params;
  newSuggestion(username, (err, data) => {
    res.json({ data })
  });
});


app.get('/suggestion/list', (req, res) => {
  const { username } = req.params;
  listSuggestions(username, (err, data) => {
    res.json({ data })
  });
});


app.listen(3000, function () {
  console.log("App started")
});

module.exports = app

The example above is how developers would normally wire their dependencies – by hardcoding them into the module. Here are some pros and cons of using this approach:

Pros:

  • Intuitive and easy to understand – you can easily understand and follow the workflow of the program because all the dependencies are statically injected.
  • Easier to debug.

Cons:

  • Modules are less flexible and are tightly coupled with their dependencies – in our example you can’t use different database instance without making the direct change to the movies module.
  • Harder to unit test – since our dependencies are hardcoded it’s harder to provide dummy data for unit tests.

Hardcoding your dependencies is acceptable and totally valid most of the time and there might be no need to over-engineer it. However, we can do better in terms of flexibility and reusability by using dependency injections.

Dependency injection

In this example we will refactor our movie suggestion service to use dependency injection instead. Let’s start off with our db connection module:

1
2
3
4
5
6
7
8
9
const { Client } = require('pg');


module.exports = (settings) => {
  const client = new Client(settings)
  client.connect();

  return client;
}

Now our module exposes a factory function instead of the instance. As you can see all the hardcoded data needed for setting up the connection is provided by the outside entity through the settings parameter. Next, let’s refactor our movie suggestion module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module.exports = (db) => {
  const movieSuggestion = {};
  movieSuggestion.listSuggestions = (username, callback) => {
    db.query(`select * from suggestions where username = '${username}'`, (err, res) => {
      //...
    });
  }


  movieSuggestion.newSuggestion = (username, callback) => {
    //...
  }

  return movieSuggestion;
}

Our main dependency – the database connection is also provided by the outside entity. Now this module provides a factory function for a movie suggestion service. Finally, let’s take a look at app module that is responsible for properly wiring all the dependencies together:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const express = require('express');
const bodyParser = require('body-parser');
const dbFactory = require('./db');
const movieFactory = require('./movies');

const app = express()
app.use(bodyParser.json())


const dbSettings = {
  user: 'dbuser',
  host: 'database.server.com',
  database: 'mydb',
  password: 'secretpassword',
  port: 3211,
};


const dbClient = dbFactory(dbSettings);
const movieSuggestion = movieFactory(dbClient);


app.get('/suggestion/new', (req, res) => {
  const { username } = req.params;
  movieSuggestion.newSuggestion(username, (err, data) => {
    res.json({ data })
  });
});


app.get('/suggestion/list', (req, res) => {
  const { username } = req.params;
  movieSuggestion.listSuggestions(username, (err, data) => {
    res.json({ data })
  });
});


app.listen(3000, function () {
  console.log("App started")
});


module.exports = app

As you can see we shifted the responsibility of setting up the dependencies from the component modules to the app module. This is one of the main concepts in dependency injection: move handling the dependencies to the higher level components, which will be responsible for distributing them to the lower-level components. The reasoning behind this is that higher level components are less likely to be reused or changed and are better adapted to tight coupling.

Pros and cons of this approach:

Pros:

  • Higher reusability – In our case it’s much easier to use our movie suggestion service with different database instances.
  • Easier unit testing – Supplying dummy data for unit testing becomes trivial.

Cons:

  • Dependencies are wired at a runtime – since our dependencies are wired to the components at the runtime it’s harder to understand the flow of the logic just by looking at the components. In our example not much complexity is added, but the bigger your app becomes the harder it will be to manage those dependencies.

Types of dependency injections

In the previous example we used one variation of dependency injection – factory injection. There are two more variations:

  • Constructor injection – when you define the dependency during the object initialization
1
const suggestion = new MovieSuggestion(db);
  • Property injection – not as robust as the first two, but this variation might be necessary sometimes when there’s a cycled dependency between the modules.
1
2
const suggestion = new MovieSuggestion();
suggestion.db = db;

Wiring modules is an important aspect of building Node.js application and it pays off to spend some time planning it. Try aiming for a high cohesion and loose coupling between the modules, especially when working with stateful modules.

In this post we discussed two methods for module wiring: hardcoding the dependencies and using dependency injection. Both of the methods have their strengths and weaknesses and it’s important to be aware of them and use them correctly based on the complexity and needs of your project.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.