Egor Panok

Full Stack JavaScript Developer

Dependency Injection in Node.JS, Part 3 - Dependency Injection Container

October 31, 2018 - 8 min read

Dependency Injection in Node.JS, Part 3 - Dependency Injection Container

Dependency Injection in Node.JS, Part 3 - Dependency Injection Container

In the last two articles we observed the DI pattern in a straightforward implementation and the flavor of DI pattern - Service Locator pattern, which has the advantage over the first one in terms convenience. On the other hand, the Service Locator pattern makes our modules bound to that pattern making it impossible to use them independently. Simply put, we sacrifice reusability over convenience. Well, fortunately, there’s a remedy. It’s the Dependency Injection Container pattern, which doesn’t make us do that concession. And by the way, it’s so relevant nowadays that Google sticks to it since 2010 in its flagship front-end framework - AngularJS and later in Angular.

Dependency Injection Container Signature

The DI Container takes the best out of two DI approaches - the straightforward one and the Service Locator. And it still remains a common registry for our components. Therefore, it inherits the public signature from to Service locator pattern, having register(name, instance), factory(name, factory), get(name) methods. Except one thing. It has a private method - inject(factory) which is used by get(name) to actually do the injection.

But let me inspect the injection in detail at the end of the article and show you how it’s used by our old use case.

Use Case

Yes, yes, I know, that’s not the first time I remind you the same use case, initially presented in the first article of this series. But anyway. We’re going to create a task tracker app. Every time the app starts along with having task endpoints we’d like to have a test DB created and initialized with random data just for unit testing. Since we have two databases we need to have the two module dependencies trees. Here’s where Dependency Injection Container comes to our aid.

Now let’s refactor the modules from the previous article to make them independent from a service locator again. Basically, we’re back to our first versions of modules before bounding them to the Service Locator. Almost.

Step 1. Refactor Database Connection module

// db-connection.js

const mongoose = require("mongoose")
mongoose.set("useCreateIndex", true)

module.exports = dbUrl => {
    return mongoose.createConnection(dbUrl, {
        useNewUrlParser: true
    })
}

As you see, back to the state, when the module can be used by straightforward dependency injection.

Step 2. Refactor Task Model

// /models/task.js

const mongoose = require("mongoose")
const Schema = mongoose.Schema

const TaskSchema = new Schema({
    name: {
        type: String,
        unique: false,
        required: true
    },
    description: {
        type: String,
        unique: false,
        required: false
    },
    completed: {
        type: Boolean,
        unique: false,
        required: true
    }
})

module.exports = connection => {
    return connection.model("Task", TaskSchema)
}

Here I refused of the idea of Model Builder, instead I export the factory function to create new Task models and I store it at the same file with TaskSchema. That simplifies the use case a bit.

Step 3. Refactor Modules for Generating and Dropping Tasks

// task-dropper.js

module.exports = TaskModel => {
    return {
        drop: () => {
            return new Promise((resolve, reject) => {
                TaskModel.deleteMany((err, res) => {
                    if (err) {
                        return reject(err)
                    }

                    return resolve(res)
                })
            })
        }
    }
}

Please note that I’m going to use the random module to generate random data.

// task-generator.js

const random = require("./../random")

module.exports = (TaskModel, generateConfig) => {
    const n = generateConfig.tasks

    return {
        generate: () => {
            return new Promise((resolve, reject) => {
                let i,
                    task,
                    counter = 0,
                    tasks = []

                for (i = 0; i < n; i++) {
                    task = new TaskModel({
                        name: random.getRandomString(20, 40),
                        description: random.getRandomString(100, 300),
                        completed: random.getRandomInteger(0, 50) > 10
                    })

                    task.save((err, t) => {
                        if (err) {
                            return reject(err)
                        }

                        counter++
                        tasks.push(t)

                        if (counter === n) {
                            return resolve(tasks)
                        }
                    })
                }
            })
        }
    }
}
// data-generator.js

module.exports = (taskDropper, taskGenerator) => {
    return {
        generate: () => {
            // Drop tasks
            taskDropper
                .drop()
                .then(() => {
                    // Generate new tasks
                    return taskGenerator.generate()
                })
                .then(tasks => {
                    for (let i = 0; i < tasks.length; i++) {
                        console.log(
                            `Task name: "${tasks[i].name}". Completed: ${tasks[i].completed}`
                        )
                    }
                })
        }
    }
}

Here please note that used task-generator and task-dropper modules as dependencies to be injected.

Step 4. Refactor The Entry Point of The App

// app.js

const express = require("express")
const app = express()
const bodyParser = require("body-parser")
const jsonParser = bodyParser.json()
const config = require("./config")
const port = config.port

// DI Container for Test Database
const testDIContainer = require("./dependency-injection-container")()
testDIContainer.register("dbUrl", config.dbUris.test)
testDIContainer.register("generateConfig", config.generate)
testDIContainer.factory("connection", require("./data-access/db-connection"))
testDIContainer.factory("TaskModel", require("./data-access/models/task"))
testDIContainer.factory("taskDropper", require("./data-gen/task-dropper"))
testDIContainer.factory("taskGenerator", require("./data-gen/task-generator"))
testDIContainer.factory("dataGenerator", require("./data-gen/data-generator"))

// Service Locator for Prod Database
const prodDIContainer = require("./dependency-injection-container")()
prodDIContainer.register("dbUrl", config.dbUris.prod)
prodDIContainer.factory("connection", require("./data-access/db-connection"))
prodDIContainer.factory("TaskModel", require("./data-access/models/task"))

// Generate Test DB
testDIContainer.get("dataGenerator").generate()

// Prod Instances
const Task = prodDIContainer.get("TaskModel")

app.use(bodyParser.urlencoded({ extended: false })) // get our request parameters
    .use(jsonParser)

app.get("/tasks", (req, res) => {
    Task.find((err, tasks) => {
        if (err) {
            return res.sendStatus(400)
        }

        return res.json(tasks)
    })
})

app.listen(port, () => console.log("Listening on port ", port))

As you see, the usage of the DI Container is identical to the usage of the Service Container. And that’s the whole point of it. We have a central registry of components. And we can have as many that registries as we want to tune our app in a smarter fashion. And look, our modules are independent from the DI Container. The DI Container is less invasive than Service Locator, which makes the dependencies modules more reusable.

Dependency Injection Container Module

Again, I’m going to share the version of DI Container module, mostly picked from this awesome book with the following minor amendment - I deliberately made the inject function private. Not sure if the authors left it public intentionally, since it wasn’t utilized in the use case from outside of the module.

// dependency-injection-container.js

const fnArgs = require("parse-fn-args")

module.exports = function() {
    const dependencies = {}
    const factories = {}
    const diContainer = {}

    diContainer.factory = (name, factory) => {
        factories[name] = factory
    }

    diContainer.register = (name, dep) => {
        dependencies[name] = dep
    }

    diContainer.get = name => {
        if (!dependencies[name]) {
            const factory = factories[name]
            dependencies[name] = factory && inject(factory)
            if (!dependencies[name]) {
                throw new Error("Cannot find module: " + name)
            }
        }
        return dependencies[name]
    }

    function inject(factory) {
        const args = fnArgs(factory).map(dependency =>
            diContainer.get(dependency)
        )
        return factory.apply(null, args)
    }

    return diContainer
}

Now it’s time to figure out what the inject(factory) method does.

And it’s pretty simple:

  1. It basically parses the names of the factory’s arguments
  2. It looks for the dependencies (inside of dependencies array) by that argument’s names
  3. It initializies the factory by dependencies.

Afterwards, the get(name) method adds the dependency returned by inject(name). Well done, now the dependency is ready to be used.

Some considerations

Consideration # 1. You might notice, that this approach relies heavily on the convention that the names of arguments in modules’ factory functions should be exactly the same as the names of dependencies and factories we use configuring our Dependency Injection Container. Otherwise, it just won’t work.

Consideration # 2. Since we rely on names, it may not work out if the minification is used. It’s not that relevant for server-side, but please note that a lot of Node modules are used both on back-end and front-end. So if you see your module is going to be used on front-end, consider other techniques (see the consideration #3).

Consideration # 3. There’re other approaches for DI Container to identify which dependencies to inject, for example:

  1. attaching a special property to the factory function:
module.exports = (a, b) => {}
module.exports._inject = ["db", "another/dependency"]
  1. specifying a module as an array of dependency names followed by the factory function
module.exports = ["db", "another/dependency", (a, b) => {}]
  1. using a comment annotation that is appended to each argument of a function (works poorly with minification).
module.exports = function (a /*db*/, b /*another/dependency*/);

Summary

We inspected Dependency Injection Container pattern. One of the hottest, sophisticated and most misunderstood pattern in the JavaScript world. Now it’s not a black magic for us anymore! We can bravely use it in our apps to decouple modules, make them more reusable and wire them in different configurations. One of the practical application for it is creating plugin-based architecture. Hope to address this topic in the next articles.

Feel free to run and experiment with the use case on you computer cloning this repository.


Written by Egor Panok, full stack JavaScript Developer who loves building useful things. Follow him on Twitter

© 2020, Egor Panok, Full Stack JavaScript Developer