IOC Container in nodejs

Magnus Tovslid
6 min readSep 17, 2017

--

In this article we’re going to explore what an IOC container is, why we need it, and how to implement it. We’re also going to look at a few dos and don’ts.

This article assumes you know what dependency injection is. I have an article on that as well:

What is an IOC container?

First of all, IOC stands for ‘inversion of control’. Dependency injection is a form of IOC, where you let someone else take control over your dependencies. An IOC container is just a tool for making this whole process more convenient. The ‘container’ is just an object where depencies are declared and stored. The declarative nature of a container is what makes it powerful. We don’t need to specify when the dependencies are created, just what they look like.

Why use an IOC container

Before explaining how to implement an IOC container it can be helpful to understand what problem it is we’re trying to solve. The problem actually arises naturally when doing dependency injection (DI). When all of your classes/functions/whatever use DI, who will do the instantiation? Everyone keeps pushing this responsibility to someone else, and before long you find that everything is instantiated together in a big lump, usually near index.js.

It may look something like this:

const config = require('./config');
const Database = require('./Database');
const UserRepo = require('./UserRepo');
const TodoRepo = require('./TodoRepo');
const Emailer = require('./Emailer');
const UserRoute = require('./UserRoute');
const TodoRoute = require('./TodoRoute');
let emailer = new Emailer(config.EMAIL_CONFIG);
let db = new Database(config.DB_CONFIG_MYSQL);
let userRepo = new UserRepo(db, emailer);
let todoRepo = new TodoRepo(db);
let userRoute = new UserRoute(userRepo);
let todoRoute = new TodoRoute(todoRepo);

This a pretty bad example since there’s not a lot going on, but you can hopefully see how this can snowball once you start adding more stuff. In particular, notice how everything has to be done in the correct order. We must instantiate in the order config -> db -> userRepo -> userRoute. This gets annoying really fast.

Implementing the container

Now we could go to NPM to find some container, but let’s keep it simple and implement it ourselves. Remember that an IOC container is declarative (or reactive as people like to say these days). We should therefore not care about the order of dependencies. Imagine we have a container already, and we want to declare the UserRepo. It could look something like this:

let c = new Container();c.service('UserRepo', c => new UserRepo(c.Database, c.Emailer));

Note: The word ‘service’ here is just a typical convention used for IOC containers, but it could just as well be called ‘declare’ or something similar.

So what did we do here exactly? Well, gave the container a function associated with the name ‘UserRepo’, which when run will return an instance of a UserRepo. The UserRepo depends on Database and Emailer, so we pick those off the container as well.

Let’s declare the rest of our dependencies as well:

c.service('config', c => config);

c.service('UserRepo', c => new UserRepo(c.Database, c.Emailer));
c.service('UserRoute', c => new UserRoute(c.UserRepo));

c.service('TodoRepo', c => new TodoRepo(c.Database));
c.service('TodoRoute', c => new TodoRoute(c.TodoRepo));

c.service('Database', c => new Database(c.config.DB_CONFIG_MYSQL));
c.service('Emailer', c => new Emailer(c.config.EMAIL_CONFIG));

In this example you can see how database and emailer are declared last, even though they are used first.

Now you might ask yourself, how do we use this? Typically, we would have some sort of App object or similar that would kick off the entire application. It would for example start an http server and listen to a port. In this example however we’re just going to assume that we want to run some function on the UserRoute. We can get an instance of the UserRoute like this:

let userRoute = c.UserRoute;
userRoute.addUser({name: 'Darth Vader'});

So if you agree with me that this is the kind of container we want, let’s look at its implementation:

class Container {
constructor(){
this.services = {};
}

service(name, cb){
Object.defineProperty(this, name, {
get: () => {
if(!this.services.hasOwnProperty(name)){
this.services[name] = cb(this);
}

return this.services[name];
},
configurable: true,
enumerable: true
});

return this;
}
}

So in just 20 lines of code we get

  1. A declarative dependency resolver
  2. Lazy instantiation
  3. A container that looks just like a regular object

The reason it looks like a regular object is that we use Object.defineProperty to run a function whenever someone tries to get a property from our container. We could have skipped this part if we wanted, but then we wouldn’t have such a nice API (we would have to run something like c.get(‘DependencyName’ instead of c.DependencyName). We add configurable: true to make it possible to override declarations, and enumerable: true so it’s possible to see what dependencies are declared.

To see why it’s declarative / lazy, look at the function run when getting a property. It runs the callback that was set when declaring the dependency, for example:

c => new UserRepo(c.Database, c.Emailer)

So inside the callback, two more properties are gotten from the container, running another set of callbacks, etc. This means that it is not until you try to get something from the container that the tree of dependencies are resolved. If a dependency is not used anywhere, it is not instantiated either.

Also, notice how each callback is only run once, with the result cached inside the services object. This means that everyone who depends on, say the Database, get’s the same object.

Providers

Even though we have solved the issue of having to care about the order of instantiation, we still have all the stuff in a single file. To remedy this, we can use a concept called providers. A provider is responsible for setting up all dependencies related to a certain part of an application or library. For example, I would have a separate provider responsible for constructing a logger (with all its different transports and rewriters and whatnot).

A provider is just a function:

// providers/userProvider.js
module.exports = function(c){
c.service('UserRepo', c => new UserRepo(c.Database, c.Emailer));
c.service('UserRoute', c => new UserRoute(c.UserRepo));
};

And the container is created from providers like this:

// createContainer.jsmodule.exports = function(){
let container = new Container();

require('./providers/loggerProvider')(container);
require('./providers/dbProvider')(container);
require('./providers/userProvider')(container);
require('./providers/todoProvider')(container);
require('./providers/appProvider')(container);

return container;
};

And used like this:

// index.js
let createContainer = require('./createContainer');

let c = createContainer();
let app = c.App;

app.start();

Testing

Unit testing is obviously easy to do when using dependency injection, but with the IOC container, it’s also a lot easier to run tests involving more parts of the application. This is because we can easily replace dependencies in the container. In the example below, we replace the database with sqlite, enabling us to run tests against the whole app:

// tests/testApp.js
let createContainer = require('./createContainer');

let c = createContainer();

c.service('Database', c => new SqliteTestDatabase());

let app = c.App;

app.start();

// Run http requests on app, and check the results

What to be careful with (avoid)

There are a couple of things you should be careful with when using IOC containers.

Adding more types of dependencies to the container

One of the things many containers have are factory-functions. Such a function produces a new instance of the dependency each time it is called. The problem with this is that if there are several types of dependencies in the container, you can never be sure what you’re gonna get. It’s just simpler to have one type, and to handle the other cases yourself. You can just as easily have a service that creates new objects for you. No need to put that burden on the container.

Passing the container around

If you start passing the container around (for other purposes than declaring things on it, like with the providers), it is not an IOC container anymore, it is a service locator. The service locator is a very bad way of handling dependencies, since it hides the real dependencies, and makes everything dependent on the service locator itself.

Putting too much stuff in the container

You don’t need to put lodash in the container. Or that small util-function without dependencies. Remember that the container is there to help you with managing dependencies.

Use decorators or comments as dependency declarations

It annoys me to no end when I see libraries such as inversifyjs allow you to declare dependencies via decorators. When you do this, you not only tie yourself to a particular container that supports this method, but you defeat the whole concept of ‘inversion of control’. Using decorators, you ask specifically for a certain dependency. Who’s in control now?

--

--