Dependency Injection (DI) with NodeJS + Typescript
Tags: javascript, node, typescript, backend
Published: 2023-07-19 17:40:12

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 - https://github.com/typestack/typedi#typedi
- Awilix - https://github.com/jeffijoe/awilix#awilix

### 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:

```typescript
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;
```

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](https://github.com/lucasprochnow2/DI-with-typedi) to check it out.

### Awilix

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

```typescript
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)
})
```

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](https://github.dev/lucasprochnow2/DI-with-awilix/blob/c26220f14e87c2995a41f7c85a6a2e92366ffcea/src/index.ts):
```typescript
import ExpressServer from "./core/server";

const expressServer = new ExpressServer();
expressServer.initialize();
```

[src/core/server](https://github.dev/lucasprochnow2/DI-with-awilix/blob/c26220f14e87c2995a41f7c85a6a2e92366ffcea/src/core/server/server.ts):
```typescript
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;
```

## Where is dependency injection initialized?

Note that [ExpressServer](https://github.dev/lucasprochnow2/DI-with-awilix/blob/c26220f14e87c2995a41f7c85a6a2e92366ffcea/src/core/server/server.ts) class constructor has a `const container = initializeInjection();` which is a function responsible for initializing the container with dependencies.

Check out [initializeInjection()](https://github.dev/lucasprochnow2/DI-with-awilix/blob/c26220f14e87c2995a41f7c85a6a2e92366ffcea/src/core/injection/injection.ts) code:

```typescript
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;
}
```

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

```typescript
export default [
  "src/domain/services/**/*.ts",
  "src/domain/repositories/**/*.ts",
  "src/rest/routes/**/*.ts",
];
```

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](https://github.dev/lucasprochnow2/DI-with-awilix/blob/c26220f14e87c2995a41f7c85a6a2e92366ffcea/src/domain/services/user/findAll.ts):

```typescript
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;
```

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](https://github.dev/lucasprochnow2/DI-with-awilix/blob/c26220f14e87c2995a41f7c85a6a2e92366ffcea/src/rest/routes/user.ts) below:

```typescript
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;
```

## 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](https://github.com/jeffijoe/awilix#awilix) before using the library in any project.

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

- [Lifetime management](https://github.com/jeffijoe/awilix#lifetime-management);
- [Injection modes](https://github.com/jeffijoe/awilix#injection-modes).

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

- TypeDI repo: https://github.com/lucasprochnow2/DI-with-typedi
- Awilix repo: https://github.com/lucasprochnow2/DI-with-awilix