DEV Community

Lucas Prochnow
Lucas Prochnow

Posted on

Dependency Injection (DI) with NodeJS + Typescript

That's a subject I rarely read a discussion or a post by NodeJS/TS community. Maybe the reason for that is the low number of libraries available to apply dependency injection in NodeJS, or the advanced features that ExpressJS (for example) provides to build a complex NodeJS API.

My main goal with this post is to show the advantages of DI in a NodeJS API and how it can scale as long as your project increases its size.

What is dependency injection

Dependency injection is a programming technique that makes a software component independent of its dependencies.

The pattern ensures that a component which wants to use a given service should not have to know how to construct those services. All the dependencies that a component needs, are provided by the injector from the dependency injection library.

DI libraries available for NodeJS

I found two libraries that are safe in any
NodeJS production environment:

TypeDI

This library uses the decorators pattern to inject dependencies into classes. I really enjoy this approach because it allows a quickly understanding while reading a class code.

Let's see a code example:

import Container, { Service } from 'typedi';

import UserRepository, { IUserRepository } from '../../repositories/user';

@Service()
class User {
  private userRepository: IUserRepository;

  constructor() {
    this.userRepository = Container.get(UserRepository);
  }

  public get() {
    return this.userRepository.get();
  }
}

export default GetUser;
Enter fullscreen mode Exit fullscreen mode

Despite my appreciation by the approach that TypeDI uses to reach dependency injection, I must confess that the documentations are very poor. Few examples and concepts are explained and the docs has some blank pages.

Even so, I developed a very simple NodeJS API to use TypeDI as injector. You can click here to check it out.

Awilix

Comparing it with TypeDI, Awilix has lots of injection options. Check the code below:

import awilix from 'awilix';

// Class
class UserController {
  userService

  constructor(deps) {
    this.userService = deps.userService
  }

  getUser(req) {
    return this.userService.getUser(req.params.id)
  }
}

// Register as a class dependency
container.register({
  userController: awilix.asClass(UserController)
})

// Factory function
const makeUserService = ({ db }) => {
  return {
    getUser: id => {
      return db.query(`select * from users where id=${id}`)
    }
  }
}

// Register as a function dependency
container.register({
  userService: awilix.asFunction(makeUserService)
})
Enter fullscreen mode Exit fullscreen mode

Awilix docs are very complete with many examples and concepts explanations. I noticed that the library creator is very active in github, always commenting, answering issues and trying to improve the library.

Hands on

I decided to use Awilix to deep dive into dependency injection for two main reasons:

  1. Awilix is very complete. It has many options to deal with dependency life cicle and different ways of injection. This variety of options is very important when the application starts scalling and problems shows up in production;

  2. My team has a NodeJS API with huge traffic running awilix in production. That's why, I am pretty sure that Awilix performs well in a distributed infrastructure.

My goal with "hands on" is not to be a step by step tutorial, but explain some thoughts, decisions and the benefits I see of using dependency injection.

I created a repo to use as reference: https://github.dev/lucasprochnow2/DI-with-awilix

Stop talking and let's go!

First doubt I had while starting the project was: The server will be in the injection or I will start the application from the server and then do the injection? (server means a "Express server" in this context).

My decision was to start the application from the server and then injecting all the dependencies into the container, leaving the server out of dependencies container.

Considering that this is a REST API application, it makes sense to me that the start of the app should be by starting Express server first, and then injecting all the dependencies into the container.

Thinking about my past experiences using Express, I've never used anything from the server object after it was started 🤔. So, my decision was based on that experience.

See below the "foundation" of the app:

src/index.ts:

import ExpressServer from "./core/server";

const expressServer = new ExpressServer();
expressServer.initialize();
Enter fullscreen mode Exit fullscreen mode

src/core/server:

import express, { Application } from "express";
import bodyParser from "body-parser";
import { AwilixContainer } from "awilix";

import RestRouters from "../../rest/routes";
import initializeInjection from "../injection";

const PORT = 3000;

class ExpressServer {
  server: Application;
  container: AwilixContainer;

  constructor() {
    const container = initializeInjection();

    this.server = express();
    this.container = container;
  }

  initializeMiddlewares() {
    this.server.use(bodyParser.json());
    this.server.use(bodyParser.urlencoded({ extended: true }));
  }

  initializeRestRouters() {
    const restRouters = new RestRouters(this.server, this.container);
    restRouters.initialize();
  }

  initialize() {
    this.initializeMiddlewares();
    this.initializeRestRouters();

    try {
      this.server.listen(PORT, (): void => {
        console.log(`Connected successfully on port ${PORT}`);
      });
    } catch (error) {
      console.error("Error occurred: ", error);
    }
  }
}

export default ExpressServer;
Enter fullscreen mode Exit fullscreen mode

Where is dependency injection initialized?

Note that ExpressServer class constructor has a const container = initializeInjection(); which is a function responsible for initializing the container with dependencies.

Check out initializeInjection() code:

import { AwilixContainer, createContainer } from "awilix";

import modulesPathList from "./modulesPathList";
import options from "./options";

export default function injection(): AwilixContainer {
  const container = createContainer();

  container.loadModules(modulesPathList, options);

  return container;
}
Enter fullscreen mode Exit fullscreen mode

Awilix allows to load all application modules by a list of relative paths. Check below the value of modulesPathList variable from the code above:

export default [
  "src/domain/services/**/*.ts",
  "src/domain/repositories/**/*.ts",
  "src/rest/routes/**/*.ts",
];
Enter fullscreen mode Exit fullscreen mode

That's one of the biggest advantages of Awilix library. A short list of three paths enables me to add all the components of my application into the injection container.

If a new service, repository or route is necessary to my application, it will be added automatically into the injection container. This injection mode allows you to focus in application logic and forget about dependency injection. That's awesome!

Let's use the dependency container

Check out the example below of the service class user/findAll:

import { IUserFindAllRepository, User } from "../../repositories/user/findAll";

export interface IUserFindAllService {
  findAll(): User[];
}

type TDeps = {
  findAllUserRepository: IUserFindAllRepository;
};

class FindAll implements IUserFindAllService {
  private findAllUser: IUserFindAllRepository;

  constructor({ findAllUserRepository }: TDeps) {
    this.findAllUser = findAllUserRepository;
  }

  public findAll() {
    return this.findAllUser.findAll();
  }
}

export default FindAll;
Enter fullscreen mode Exit fullscreen mode

That service can only access dependencies if it is within dependencies container. Scrolling back to the previous session, you can see that we added all the services in the container through "src/domain/services/**/*.ts" relative path.

By default awilix exports all injected components in the class contructor function, as you can see in the code above. But awilix is not limited only to classes, it can handle factory functions as well, as you can see in the code below:

import { Router, Request, Response } from "express";
import { IUserFindAllService } from "../../domain/services/user/findAll";
import { IUserFindByIdService } from "../../domain/services/user/findById";

type TDeps = {
  findAllUserService: IUserFindAllService;
  findByIdUserService: IUserFindByIdService;
};

const userRoutes = (deps: TDeps) => {
  const { findAllUserService, findByIdUserService } = deps;
  const router = Router();

  router.get("/", (_: Request, res: Response) => {
    const getUser = findAllUserService.findAll();
    res.status(200).json(getUser);
    return;
  });

  router.get("/:id", (req: Request, res: Response) => {
    const getUser = findByIdUserService.findById(req.params.id);
    res.status(200).json(getUser);
    return;
  });

  return router;
};

export default userRoutes;
Enter fullscreen mode Exit fullscreen mode

Conclusion

There are many other options and resources that Awilix library can offer to inject dependencies in your project. I strongly recommend to read the docs before using the library in any project.

That's my suggestion of next topics to deep dive about Awilix:

Links of the repos I used to test dependency injection libraries:

Top comments (0)