Dependency Injection in Node.js

RisingStack's services:

Sign up to our newsletter!

In this article:

Dependency injection is a software design pattern in which one or more dependencies (or services) are injected, or passed by reference, into a dependent object.

Reasons for using Dependency Injection

Decoupling

Dependency injection makes your modules less coupled resulting in a more maintainable codebase.

Easier unit testing

Instead of using hardcoded dependencies you can pass them into the module you would like to use. With this pattern in most cases, you don’t have to use modules like proxyquire.

Faster development

With dependency injection, after the interfaces are defined it is easy to work without any merge conflicts.

How to use Dependency Injection using Node.js

First, let’s take a look at how you could write your applications without using dependency injection, and how would you transform it.

Are you looking for help with enterprise-grade Node.js Development?

Hire the Node.js experts of RisingStack!

Sample module without dependency injection

// team.js
var User = require('./user');

function getTeam(teamId) {
  return User.find({teamId: teamId});
}

module.exports.getTeam = getTeam;

A simple test would look something like this:

// team.spec.js
var Team = require('./team');
var User = require('./user');

describe('Team', function() {
  it('#getTeam', function* () {
    var users = [{id: 1, id: 2}];

    this.sandbox.stub(User, 'find', function() {
      return Promise.resolve(users);
    });

    var team = yield team.getTeam();

    expect(team).to.eql(users);
  });
});

What we did here is that we created a file called team.js which can return a list of users who belong to a single team. For this, we require the User model, so we can call its find method that returns with a list of users.

Looks good, right? But when it comes to testing it, we have to use test stubs with sinon.

In the test file, we have to require the User model as well, so we can stub its find method. Notice, that we are using the sandbox feature here, so we do not have to manually restore the original function after the test run.

Note: stubs won’t work if the original object uses Object.freeze.

Sample module with dependency injection

// team.js
function Team(options) {
  this.options = options;
}

Team.prototype.getTeam = function(teamId) {
  return this.options.User.find({teamId: teamId})
}

function create(options) {
  return new Team(options);
}

You could test this file with the following test case:

// team.spec.js
var Team = require('./team');

describe('Team', function() {
  it('#getTeam', function* () {
    var users = [{id: 1, id: 2}];
    
    var fakeUser = {
      find: function() {
        return Promise.resolve(users);
      }
    };

    var team = Team.create({
      User: fakeUser
    });

    var team = yield team.getTeam();

    expect(team).to.eql(users);
  });
});

Okay, so how the version with dependency injection differs from the previous one? The first thing that you can notice is the use of the factory pattern: we use this to inject options/dependencies to the newly created object – this is where we can inject the User model.

In the test file we have to create a fake model that will represent the User model then we simply inject this by passing it to the create function of the Team model. Easy, right?

Dependency Injection in Real Projects

You can find dependency injection examples in lots of open-source projects. For example, most of the Express/Koa middlewares that you use in your everyday work uses the very same approach.

Express middlewares

var express = require('express');
var app = express();
var session = require('express-session');

app.use(session({
  store: require('connect-session-knex')()
}));

The code snippet above is using dependency injection with the factory pattern: to the session middleware we are passing the connect-session-knex module – it has to implement an interface, that the session module will call.

In this case the connect-session-knex module has to implement the following methods:

  • store.destroy(sid, callback)
  • store.get(sid, callback)
  • store.set(sid, session, callback)

Hapi plugins

The very same concept can be found in Hapi as well – the following example injects the handlebars module as a view engine for Hapi to use.

server.views({
  engines: {
    html: require('handlebars')
  },
  relativeTo: __dirname,
  path: 'templates'
});

Recommended reading

Node.js Best Practices – Part 2:
The next chapter of Node.js best practices, featuring pre-commit checks, JavaScript code style checker and configuration best practices.

Do you use dependency injection in your projects? If so, how? Please share your thoughts, projects or examples in the comments below.

Share this post

Twitter
Facebook
LinkedIn
Reddit