DEV Community

Cover image for NodeJs - Dependency injection, make it easy
Vıɔk A Hıƃnıʇɐ C.
Vıɔk A Hıƃnıʇɐ C.

Posted on • Updated on

NodeJs - Dependency injection, make it easy

If when you are working with #NodeJs and #TypeScript you have not experienced mutation problems is a sign that you are doing things right or maybe you do not know what you are doing and in the latter case you have been playing with luck, good luck of course.

Based on the previous premise it becomes necessary a correct handling of the import of the modules that your application will use to create the pipeline where the requests will enter your application, and for this there are three ways, or at least I know three ways and each one brings with it its implications.

We have the following options:

  1. Import module as a default instance (Singleton)
  2. Import the types and create instances (transient) in the context where they will be used, usually a PrimaryAdapter or in simple words the entry point of that execution.
  3. Create an IoC Container (Inversion of control) or Dependency Container.

Option 1:

It is the simplest way to do it, but it is the least indicated because if you do not make a good implementation, you will probably have Mutations problems, a headache when working with JS.

In some directory you will have something like this:

import { awesomeProvider, amazingProvider } from "../../providers/container";
import { AwesomeUseCase } from "../../../../application/modules/oneModule/useCases/awesome";
import { AmazingUseCase } from "../../../../application/modules/twoModule/useCases/amazing";

const awesomeUseCase = new AwesomeUseCase(awesomeProvider);
const amazingUseCase = new AmazingUseCase(amazingProvider);

export { awesomeUseCase, amazingUseCase };
Enter fullscreen mode Exit fullscreen mode

And in your controller some like as follows:

import BaseController, { Request, Response, NextFunction } from "../base/Base.controller";
import { amazingUseCase, awesomeUseCase } from "./container/index";

class YourController extends BaseController {
  constructor() {
    super();
    this.initializeRoutes();
  }

  amazing = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      this.handleResult(res, await amazingUseCase.execute());
    } catch (error) {
      next(error);
    }
  };

  awesome = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      this.handleResult(res, await awesomeUseCase.execute());
    } catch (error) {
      next(error);
    }
  };

  protected initializeRoutes(): void {
    this.router.get("v1/amazing", this.amazing);
    this.router.get("v1/awesome", this.awesome);
  }
}

export default new YourController();
Enter fullscreen mode Exit fullscreen mode

The problem with the previous implementation is that if the modules you export have global variables and in the case that two concurrent requests arrive at the same entry point, you will most likely have mutations, why?, because that's how JS works.

Option 2:

This way is the most indicated, but you would dirty a lot your Adapter or entry point (Controller) with imports of all kinds, because most likely your module requires the injection of other dependencies and you would have to do a management of those instances, something cumbersome, and yes, I know you are probably thinking that you would create an index file and there you would do all that heavy work for the resources of the main instance, but it is still dirty, let's see:

import BaseController, { Request, Response, NextFunction } from "../base/Base.controller";
import { awesomeProvider, amazingProvider } from "../providers/container";
import { AwesomeUseCase } from "../../../application/modules/oneModule/useCases/awesome";
import { AmazingUseCase } from "../../../application/modules/twoModule/useCases/amazing";

class YourController extends BaseController {
  constructor() {
    super();
    this.initializeRoutes();
  }

  amazing = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const amazingUseCase = new AmazingUseCase(amazingProvider);
      this.handleResult(res, await amazingUseCase.execute());
    } catch (error) {
      next(error);
    }
  };

/* other entry points */

  protected initializeRoutes(): void {
    this.router.get("v1/amazing", this.amazing);
    this.router.get("v1/awesome", this.awesome);
  }
}

export default new YourController();
Enter fullscreen mode Exit fullscreen mode

And depending on the amount of modules you use, this can grow in ways you can't even imagine, and that's where I'm going with the next point I want to share with you.

Option 3:

This way is one of the ContainerPatterns and basically is a Container like the IoC ones but more versatile, since you can handle from inverted dependencies or concrete classes that don't have defined contracts, so without further ado (Shit) let's go to the code.

Everything starts from a Class called Container
and a contract as a dictionary of type IContainerDictionary where we will relate our dependencies, whether Class with or without contracts (Interface) defined, and as we can see we have a get method that receives a type which will serve to manage it.

import { ApplicationError } from "../../application/shared/errors/ApplicationError";
import resources, { resourceKeys } from "../../application/shared/locals/messages";
import applicationStatus from "../../application/shared/status/applicationStatus";

export class Container {
  constructor(private readonly container: IContainerDictionary) {}

  get<T>(className: string): T {
    if (!this.container[className]) {
      throw new ApplicationError(
        resources.getWithParams(resourceKeys.DEPENDENCY_NOT_FOUNT, { className }),
        applicationStatus.INTERNAL_ERROR,
      );
    }

    return this.container[className]() as T;
  }
}

export interface IContainerDictionary {
  [className: string]: NewableFunction;
}
Enter fullscreen mode Exit fullscreen mode

The Container Class should be part of the Infrastructure layer of your solution, it should have nothing to do neither with the Application and/or Domain layer of your solution speaking in terms of a Clean Architecture based solution, even in a N Layers.

To use this pattern we go to our Adapters layer, where is our entry point, usually a Controller, there we create a directory called container and in this a file index, and there we would have something like the following code:

import { Container, IContainerDictionary } from "../../../../infrastructure/ioc/Container";
import { AwesomeUseCase } from "../../../../application/modules/one/useCases/awesome";
import { AmazingUseCase } from "../../../../application/modules/two/useCases/amazing";
import { awesomeProvider, amazingProvider } from "../../../providers/container/index";

const dictionary: IContainerDictionary = {};
dictionary[AwesomeUseCase.name] = () => new AwesomeUseCase(awesomeProvider);
dictionary[AmazingUseCase.name] = () => new AmazingUseCase(amazingProvider);

export { AwesomeUseCase, AmazingUseCase };
export default new Container(dictionary);
Enter fullscreen mode Exit fullscreen mode

And once we have our container then we can make use of it in our controllers as follows:

import BaseController, { Request, Response, NextFunction } from "../base/Base.controller";
import container, { AmazingUseCase, AwesomeUseCase } from "./container/index";

class YourController extends BaseController {
  constructor() {
    super();
    this.initializeRoutes();
  }

  amazing = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      this.handleResult(res, await container.get<AmazingUseCase>(AmazingUseCase.name).execute());
    } catch (error) {
      next(error);
    }
  };

  awesome = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      this.handleResult(res, await container.get<AwesomeUseCase>(AwesomeUseCase.name).execute());
    } catch (error) {
      next(error);
    }
  };

  protected initializeRoutes(): void {
    this.router.get("v1/amazing", this.amazing);
    this.router.get("v1/awesome", this.awesome);
  }
}

export default new YourController();
Enter fullscreen mode Exit fullscreen mode

Now, how this works:
The key to everything is in the anonymous functions that we create in the dependencies dictionary, since when the get method of the Container is invoked, what we do is to execute this anonymous function so that it returns us the corresponding new instance (transient), something simple but powerful because it also optimizes the memory management of our applications, since there are no instances of any type of dependencies until the moment a request enters our entry point, and once the request has finished, the resources will be released because the execution context in the Call stack will have ended.

It is worth noting that there are packages that do this, some of them are inversify, awilix, typedi, among others.

This article is the explanation of a thread I launched a few days ago on twitter (@ktrehttps) (Spanish: https://twitter.com/ktrehttps/status/1429327992182956033?s=20) and while I was writing them it occurred to me that we can increase the possibilities of the container but that will be in another possible post.

I hope the reading of this post has been enriching for the continuous learning path that requires to be dev. :) 8^

Discussion (0)