Dependency Injection in Node.JS

Sep 17, 2013

By Joshua Holbrook

Dependency injection frameworks are relatively uncommon in node. On the client, Angular uses dependency injection, but the learning curve of Angular is so steep that the core of dependency injection ends up being obscured by all the other concepts. Generally, javascripters think of dependency injection as "that thing Martin Fowler invented."

Despite its reputation as an obscure Design Pattern, dependency injection is a pretty good idea. In fact, i.TV wrote their own dependency injection framework, dependable, when they realized they were doing it manually throughout their application code. By using dependency injection, we've been able to clean up a bunch of boilerplate code, keep our codebase tidy, and solve Real Problems in application development.

Dependency Injection 101

The basics of dependency injection are actually pretty simple, and it's likely you do it already.

There are three core elements to dependency injection, according to wikipedia, and they seem reasonable enough:

  1. Some consumer. This is the thing that has dependencies.
  2. Some dependencies. These are the things that the consumer needs in order to work. This entails the consumer expecting some particular API on the part of the dependency.
  3. Some injector. The injector is the thing that connects the consumer to its dependencies.

This is pretty vague, mostly because there are a lot of ways in which this pattern can be expressed. For a more concrete example of minimal non-fancy dependency injection, you may have some model code that needs access to the same database client:

var config = require('../config.json');

var User = require('./models/user'),
    Group = require('./models/group'),
    Post = require('./models/post');

var Client = require('./lib/client');

var client = Client(config);

var user = User(client),
    group = Group(client),
    post = Post(client);

Here, the consumers are our models, the dependency is our client, and the injector is simply the part where we manually require everything and pass the configured parts along. Our consumers depend on an object which implements the client instance's API, and that dependency is injected into each model by passing it to each model's constructor.

There are a few nice features of this approach. The most obvious is that we're able to pass the same client instance to all of our models, which is good because they should all speak to the same database over the same connection. Another feature that may be overlooked is that you can inject anything as long as it satisfies the API contract; that is, it has the methods that the consumer expects. This is really handy when you want to use a mock in testing:

var assert = require('assert');

//
// Require our mock
//
var client = require('./mocks/client');

var User = require('../models/user');

describe('user model', function () {

  //
  // Inject our mock
  //
  var user = User(client);

  it('can get a user', function (done) {
    //
    // Inline configuration of our mock client
    //
    client.set('wendy', {
      _id: 'wendy'
    });

    //
    // Our model is using our mock client
    //
    user.get('wendy', function (err, doc) {
      assert(!err);
      assert.equal(doc._id, 'wendy');
      done();
    });
  });
});

Getting Fancy

This approach works, but the experience could be better. After all, you're having to manually require different components and wire them together. For only a few dependencies/consumers this isn't so bad. For nested dependencies (What happens when your controller depends on all your models?), having to juggle all these objects around can become a chore.

The inevitable conclusion is a library, often called a "dependency injection framework", which is designed to automate the process of injecting the right dependencies into the right consumers at the right time. The dependency injection framework i.TV wrote to solve this problem, Dependable, has an api that looks like this:

var container = require('dependable').container;

var User = require('./models/user'),
    Group = require('./models/group'),
    Post = require('./models/post');

var Client = require('./lib/client');

container.register('config', require('../config.json'));
container.register('client', function (config) { 
  return Client(config);
});

container.register('user', User);
container.register('group', Group);
container.register('post', Post);

container.resolve(function (user, group, post) {
  //
  // `user`, `group` and `post` are all properly configured
  //
});

(If you've used angular.js, this probably looks somewhat familiar.)

Take a close look at what's happening here:

  • config is being specified as an object hash
  • client takes config as a dependency, and is registered as an instance of Client
  • user, group and post are all defined as functions of client which return their respective model instances

You can get a more in-depth look at Dependable's API by checking out its README.

Keep in mind that this is just a particular implementation of dependency injection. In order to automatically inject dependencies, whichever framework we use has to know two things:

  1. Which dependencies does a given consumer have?
  2. Which dependencies does a given consumer fulfill?

How these are specified is a function of not just taste, but also constraints from the language, toolchain, or other requirements of the software. For example, dependency injection in static languages often involves XML-based metadata or Java annotations, In our case, (2) is handled by the first argument to container.register. The way (1) is handled, however, is a bit more magical: Dependable parses the names of dependencies out of the call signature by stringifying the function. This is possible only because JavaScript allows for stringifying functions in the first place, and gives us a relatively terse syntax. Because our library targets node.js, we don't have to worry about minification changing the dependency names like Angular does, nor are we forced to introduce more cumbersome annotation systems.

To revisit an earlier example: Here's how tests mocks can be handled with dependable:

var assert = require('assert');

//
// Assume that the container from the previous example
// is exposed as a module export
//
var container = require('./container');

var client = require('./mocks/client');

describe('user model', function () {

  //
  // Inject our mock
  //
  container.register('client', function () {
    return client;
  });

  it('can get a user', function (done) {
    client.set('wendy', {
      _id: 'wendy'
    });

    //
    // Resolve the user model
    //
    container.resolve(function (user) {
      //
      // Our model is using our mock client
      //
      user.get('wendy', function (err, doc) {
        assert(!err);
        assert.equal(doc._id, 'wendy');
        done();
      });
    });
  });
});

This should look similar to the original testing example. Here, though, we're able to redefine a single dependency and count on dependable to properly inject it as necessary. Given nested dependencies, this is way easier than having to re-inject the whole chain of dependencies manually.

The Take-Home

The obvious lesson here is that design patterns (or at least dependency injection) are actually useful in that, if you catch yourself writing code that fits the pattern, you can apply existing knowledge and experience regarding that pattern. Of course, that's the entire point of design patterns.

More specifically, when we catch ourselves manually injecting dependencies in node, we can just require dependable and use it to manage the process. This gives the advantages of dependency injection (such as easily sharing instances and being able to easily replace dependency implementations), while handling the issues that crop up when doing it manually (such as boilerplate, and managing nested dependency trees).

Dependable on Github

Joshua Holbrook discovered JavaScript in college while avoiding his homework and turned it into a career. He likes science, math, computers, and the frigid Northern wastes of Alaska, where he was born and raised. Josh lives in South Salt Lake with his budgie, Korben.