Egor Panok

Full Stack JavaScript Developer

Dependency Injection in Node.JS, Part 1 - Put It Simple

October 06, 2018 - 8 min read

Dependency Injection in Node.JS, Part 1 - Put It Simple

The Dependency Injection (DI) pattern is quite popular nowadays in the JS world and probably the most misunderstood one. In this article I’d like to start talking about it, describing its very basic version.

Purposes - Reusability and Unit Testing

The conventional way of programming in Node.JS prescribes loading dependencies by means of require() function. And it’s nothing wrong with it until you get to creating big application where the relevance of creating a reusable modules and unit testing starts playing a decent role.

Dependency Injection In a Nutshell

Consider a big modules graph where modules are wired to each other having their dependencies hardcoded. For example, we have a module, representing a database connection instance, which is wired to other modules by require() function. In this case there’s no way to reuse those dependent modules with a different database connection instance.

So, the essence of Dependency Injection pattern is simply that we instantiate stateful dependencies beforehand and then pass (or inject) the dependencies to other modules as parameters.

This provides us a loose-coupling, allowing reusing the modules with different dependencies and easily unit test them. We can easily create other instances of dependencies and pass them to our modules.

Straightforward Dependency Injection - Use Case

Suppose, we want to create a simple task tracker API application using Node.JS, Mongo, Mongoose. We also want to implement unit tests that will be run on the database with some test data. In other words, we’ll have 2 databases - for the app itself and for running unit tests. The database with the test data should be generated on the fly just when the application starts. Creating the unit tests themselves is out of the scope of use case - we just want to create a foundation for it - generating the database with test data on the fly.

Let’s start creating the app.

Problem

Mongoose works with data models to manipulate data. And the problem is that the data-models are bound to specific database connections. So, if we simply reference a mongoose module at creating our Task model, then the model will use the default connection, created by mongoose.connect(), called in the app.

But the task prescribes having two database connections - for the real app itself and the connection with test data (to run units tests). And here’s when we need to have a general module allowing us to create connections to different databases.

Step 1. Database Connection Module

Alright, let’s implement a module, creating a connection to a specific database by its url.

// dbConnection.js

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

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

As you see, it exports a factory function, creating the connection by url. And it’s just what we need to create the instances of DB connections, which themselves will be used as dependencies later on.

Step 2. Mongoose Task Schema

Here’s a very basic Mongoose schema for the tasks:

// schemas/task.js

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

module.exports = new Schema({
    name: {
        type: String,
        unique: false,
        required: true
    },
    description: {
        type: String,
        unique: false,
        required: false
    },
    completed: {
        type: Boolean,
        unique: false,
        required: true
    }
})

The module is stateless. It doesn’t export any factory function to create different schemas for different databases. Instead, the schemas are the same across multiple databases we might want to create. Which is not the case for data models.

Step 3. Mongoose Model Builder

Similarly to the database connection schema, here’s the module with a factory function, allowing us to create on the fly Mongoose models, based on connection, model name and Mongoose schema.

// model.js
module.exports = (connection, modelName, schema) => {
    return connection.model(modelName, schema)
}

Step 4. Modules for Generating And Dropping Tasks

Now we’re almost ready to generate tasks for the test database. But before it, let’s write a module providing random integers and random strings.

// random.js

function getRandomInteger(min, max) {
    return Math.floor(Math.random() * (max - min)) + min
}

function getRandomString(minLengh, maxLength) {
    const possible =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
    const length = getRandomInteger(minLengh, maxLength)
    let text = ""

    for (let i = 0; i < length; i++) {
        text += possible.charAt(Math.floor(Math.random() * possible.length))
    }

    return text
}

module.exports = {
    getRandomInteger,
    getRandomString
}

Alright, now, let’s create a module to generate tasks. It accepts TaskModel as a dependency, which itself should have been instantiated by database connection instance as dependency.

// task-generator.js

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

module.exports = (TaskModel, n) => {
    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)
                        }
                    })
                }
            })
        }
    }
}

We need to drop tasks before creating them. For this purpose we wrote the TaskDropper module with the similar approach - take the Task model as input and then use it to delete all 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)
                })
            })
        }
    }
}

And as the final point here we’d like to create a common module for creating a database with test data. Currently it will drop and re-create only tasks but later on it will be expanded to drop and re-create other entities, like projects, clients, etc. It will take as an input a database connection and a configuration object with the information how many entities of each type should be created.

// data-generator.js

const TaskSchema = require("../data-access/schemas/task")

// Factories
const modelFactory = require("../data-access/model")
const taskGeneratorFactory = require("./task-generator")
const taskDropperFactory = require("./task-dropper")

module.exports = (connection, generateConfig) => {
    // Instances
    const Task = modelFactory(connection, "Task", TaskSchema)
    const tasksGenerator = taskGeneratorFactory(Task, generateConfig.tasks)
    const taskDropper = taskDropperFactory(Task)

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

Step 5. The Entry Point of The App

Here is the entry point of our application. We’re loading factories to create dependencies which we’re using to pass in our re-usable modules.

// 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
const TaskSchema = require("./data-access/schemas/task")

// Factories
const dbConnectionFactory = require("./data-access/dbConnection")
const modelFactory = require("./data-access/model")
const dataGeneratorFactory = require("./data-gen/data-generator")

// Prod Instances
const connection = dbConnectionFactory(config.dbUris.prod)
const Task = modelFactory(connection, "Task", TaskSchema)

// Test Instances
const testConnection = dbConnectionFactory(config.dbUris.test)
const dataGenerator = dataGeneratorFactory(testConnection, config.generate)

// Generate Test DB
dataGenerator.generate()

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))

Let’s examine the profit of the re-usability in the app:

  1. dbConnection module is re-used twice - to create connection to prod database and test database which is passed then as a dependency to many other instances which themselves act as dependencies;
  2. model module is used twice in the app - for getting access to the tasks in the prod db and in test db. But in fact this general module can be used later on for creating models for other entities: projects, clients, etc., when our app grows;
  3. dataGenerator, taskGenerator and taskDropper are used ones but they are reusable too. They can be easily used to generate new database with different dbURI.

If you’re interested to play around the code - feel free to. Here’s the repository.

Summary

The Dependency Injection pattern is not magical and the basic version of it looks very straightforward. It also seem very similar to parameterizing functions and serves the same goal - re-usability. Using DI we comply with the DRY (Don’t Repeat Yourself) principle and it actually allows us achieving loose-coupling which is a decent criteria of good software design.

Credits: Photo by Marvin Meyer on Unsplash.


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

© 2020, Egor Panok, Full Stack JavaScript Developer