Injeção de dependência (DI) com NodeJS + Typescript
Lucas Prochnow
Posted on July 3, 2023
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 - https://github.com/typestack/typedi#typedi
- Awilix - https://github.com/jeffijoe/awilix#awilix
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;
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)
})
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:
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;
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:
import ExpressServer from "./core/server";
const expressServer = new ExpressServer();
expressServer.initialize();
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;
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;
}
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",
];
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;
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;
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:
- Repo usando TypeDI: https://github.com/lucasprochnow2/DI-with-typedi
- Repo usando Awilix: https://github.com/lucasprochnow2/DI-with-awilix
Posted on July 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.