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.jsNode.js is an asynchronous event-driven JavaScript runtime and is the most effective when building scalable network applications. Node.js is free of locks, so there's no chance to dead-lock any process. Development?
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.