Injeção de dependência (DI) com NodeJS + Typescript

lucasprochnow

Lucas Prochnow

Posted on July 3, 2023

Injeção de dependência (DI) com NodeJS + Typescript

Um assunto pouco abordado na comunidade NodeJS/TS, é injeção de dependências em projetos backend.

Tenho a impressão que é uma prática que não avançou tanto nos ambientes backend nodeJS e eu sinceramente não sei o motivo. Talvez esteja relacionado com a baixa quantidade de bibliotecas disponíveis para aplicar injeção em projetos nodeJS, não sei.

Mas meu objetivo com esse post é mostrar um pouco das vantagens dessa prática para a aplicação nodeJS e como ela pode deixar seu projeto escalável, sem abrir mão de organização e claro, testes unitários.

O que é injeção de dependência

Como o próprio nome sugere, a injeção de dependência é um padrão de desenvolvimento de software onde um componente de software recebe outro componente como dependência.

O objetivo principal da injeção de dependência é remover o acoplamento entre os componentes. Uma classe que receba outra classe por injeção, não sabe como construir a classe injetada, essa construção fica abstraída no "injetor" de dependência.

Acredito que o entendimento desse conceito ficará mais simples com o código que vamos desenvolver a seguir.

Bibliotecas DI diponíveis para nodeJS

Encontrei duas bibliotecas que são seguras para qualquer ambiente nodeJS em produção:

TypeDI

Essa biblioteca adotou o padrão "decorators" para injetar as dependências nas classes. Particularmente, eu gosto bastante dessa abordagem pois batendo o olho na classe, eu consigo saber que ela está usando DI. Exemplo de injeção usando TypeDI:

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

Apesar de eu gostar mais dessa abordagem que a lib TypeDI propõem, confesso que a documentação deles deixa bastante a desejar. Possui poucos exemplos e algumas páginas estão em branco, sem explicação dos conceitos.

Deixo aqui o link de um projetinho node no github que usa o TypeDI para injeção de dependência. Criei esse repositório para entender melhor a lib na prática.

Awilix

Tem uma proposta bem parecida com a biblioteca acima, mas ela nos dá mais opções na hora de injetar as dependências dentro de um Container. Veja os exemplos abaixo:

import awilix from 'awilix';

// Class
class UserController {
  userService

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

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

// Registra a dependência como classe
container.register({
  userController: awilix.asClass(UserController)
})

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

// Registra a dependência como função
container.register({
  userService: awilix.asFunction(makeUserService)
})
Enter fullscreen mode Exit fullscreen mode

Ao contrário da lib TypeDI, a documentação do Awilix é (muito) mais completa, com vários exemplos práticos e explicação de cada conceito. O criador do Awilix é bem ativo no github, sempre respondendo as issues e ajudando o pessoal que usa a lib, o que dá mais pontos para essa lib, na minha opinião.

Mão na massa

Para exemplificar de forma prática a injeção de dependência em um projeto nodeJS, eu decidi usar a lib Awilix por dois motivos principais:

  1. Robustês: Ela possui várias opções para gerenciar o ciclo de vida da injeção e alguns modos de injeção diferentes. Essa variedade de opções é muito importante quando a aplicação começa a escalar e problemas começam a aparecer em produção;

  2. Já provei que a lib performa muito bem em produção. Ela roda em um microsserviço node que recebe centenas de milhares de requisições por dia.

O objetivo dessa etapa, não é ser um passo a passo de como criar uma api node com injeção de dependência do zero, mas sim explicar alguns pontos principais, tomadas de decisão que precisei fazer e vantagens da injeção na aplicação como um todo. Obviamente, tudo isso é minha opinião baseado nas experiências que tive.

Para isso, preparei um projeto que vou usar como referência aqui: https://github.dev/lucasprochnow2/DI-with-awilix

Chega de papo furado e vamos lá!!

Primeira dúvida que tive ao iniciar o projeto: O server vai entrar na injeção ou vou iniciar a aplicação a partir do server? (Entenda server aqui como um "Express Server" em uma api node).

Minha decisão foi iniciar a aplicação a partir do server e só então injetar todas as dependências no container, deixando o server fora do container de injeção.

Decidi seguir por esse caminho pois, partindo do princípio que essa aplicação é uma api REST, faz mais sentido na minha cabeça iniciarmos a aplicação construindo toda a "fundação" primeiro, que pra mim é o server e a partir dessa "fundação" construir o resto da aplicação.

Veja, você pode entender que a "fundação" dessa aplicação é o container com as dependências. Não está errado, é só outro jeito de "ver o mundo" hahaha.

Veja abaixo como ficou a "fundação" da api node:

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

Onde acontece a injeção das dependências?

Se você observar o constructor da classe ExpressServer, que está representada na sessão anterior, ele possui um const container = initializeInjection(); que é responsável por inicializar todo o container com as dependências do projeto.

Veja que essa função initializeInjection() faz:

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

O Awilix permite carregar módulos da aplicação passando uma lista de caminhos relativos dos módulos que você deseja carregar. Veja abaixo o que a variável modulesPathList do trecho acima representa:

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

Aqui está uma das grandes vantagens do Awilix com relação às outras libs de injeção nodeJS. Com poucas linhas de código, eu consigo ter todos os componentes da minha aplicação injetados no container.

Outra vantagem dessa abordagem é que tudo que for adicionado dentro desses três caminhos no futuro, será automaticamente injetado no container. Isso evita com que alguma mudança tenha que ser feita no contexto da injeção, caso um novo service for implementado, por exemplo.

Como eu uso o container com as dependências agora?

Com o container de injeção construído direitinho, agora nós conseguimos usar os módulos que estão no container.

Como exemplo, vou usar a classe do service user/findAll, mas você pode observar que os outros services são bem parecidos.

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

Bom, para que esse service possa enxergar os módulos que estão no container de dependências, ele também precisa estar dentro do container. Se voltarmos na sessão anterior, nós adicionamos a camada services dentro do container através do caminho "src/domain/services/**/*.ts".

Esse service de exemplo tem por objetivo retornar todos os usuários da fonte de dados e para isso ele precisa perguntar para o repositório de usuários quais são esses usuários. Portanto, o componente que ele precisa acessar do container de dependências é o repositório de usuários.

Por padrão o Awilix expõem os componentes injetados, nos parâmetros dos construtores da classe. Veja que na classe acima, estamos descontruíndo os argumentos do constructor e pegando apenas a função findAllUserRepository. Essa função foi carregada na sessão anterior, quando declaramos o caminho "src/domain/repositories/**/*.ts" para o injetor.

Um ponto muito positivo do Awilix é que ele não se limita a injetar as dependências apenas em componentes de classe. Ele consegue injetar também em funções factory. Veja o exemplo abaixo do roteador de usuários:

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

Repare que o roteador da rota /user acima, precisa de duas dependências: findAllUserService e findByIdUserService.

Conclusão

Meu objetivo com esse post foi trazer uma ideia e demonstrar quais são as vantagens de usar injeção de dependências em projetos nodeJS.

O Awilix tem muito mais opções e detalhes para serem explorados. A lib é bem completa e cheia de recursos. Recomendo fortemente a leitura das docs antes de começar a usar de fato em qualquer projeto.

Deixo minha sugestão dos próximos tópicos para vocês estudarem sobre o Awilix. Quem sabe posto outros conteúdos aprofundando nesses tópicos futuramente:

Deixei os links dos projetos que usei de base para esse post, mas vou deixá-los novamente abaixo:

💖 💪 🙅 🚩
lucasprochnow
Lucas Prochnow

Posted on July 3, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related