DEV Community

Cover image for Inversão de Controle (IoC) e Injeção de Dependência em APIs Express com TypeScript
Vitor Rios
Vitor Rios

Posted on • Updated on

Inversão de Controle (IoC) e Injeção de Dependência em APIs Express com TypeScript

Introdução

A arquitetura de software robusta e sustentável é um objetivo crucial para qualquer projeto de desenvolvimento. No ecossistema de Node.js, isso não é diferente. A aplicação dos princípios de Inversão de Controle (IoC) e Injeção de Dependência (DI) pode elevar significativamente a qualidade do código em projetos TypeScript. Ao utilizar esses princípios em conjunto com o Express, criamos uma fundação sólida para aplicações escaláveis e fáceis de manter.

O que é Inversão de Controle?

Inversão de Controle é um princípio de design de software que desacopla os componentes de uma aplicação, transferindo o controle de fluxos e dependências para um 'container' ou framework. Em vez de um componente criar ou buscar as dependências necessárias, ele as recebe de uma fonte externa, geralmente um framework ou biblioteca especializada.

E Injeção de Dependência?

Injeção de Dependência é uma técnica de IoC onde um objeto recebe outras instâncias de objetos (dependências) de que necessita. Em vez de criar suas dependências internamente ou buscar globalmente, o objeto tem suas dependências 'injetadas' no momento da criação, geralmente por meio de construtores, métodos ou propriedades.

Implementação com TypeScript

Vamos explorar como implementar IoC e DI em uma API Express estruturada com rotas, controladores, serviços e repositórios.

Setup Inicial

Instale o pacote inversify, uma biblioteca IoC para TypeScript, e habilite os decoradores no seu tsconfig.json:

npm install inversify reflect-metadata
Enter fullscreen mode Exit fullscreen mode
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Criando o Container de Inversão de Controle

Usamos um container IoC para gerenciar nossas dependências. Aqui, definimos as 'bindings' que mapeiam as interfaces às suas implementações concretas.

inversify.config.ts

import { Container } from 'inversify';
import "reflect-metadata";
import { UserService } from './services/UserService';
import { UserRepository } from './repositories/UserRepository';

const container = new Container();
container.bind<UserService>('UserService').to(UserService);
container.bind<UserRepository>('UserRepository').to(UserRepository);

export { container };
Enter fullscreen mode Exit fullscreen mode

É necessário adicionar o bind no container de injeção de dependência para todas as classes que você deseja que sejam injetáveis. Além disso, o uso do decorador @injectable é essencial em cada classe injetável. Isso garante que você utilize sempre a mesma instância da classe em toda a aplicação, promovendo a reutilização e evitando a criação de múltiplas instâncias, o que é um princípio chave na injeção de dependência e na inversão de controle.

Aplicando a Injeção de Dependência

services/UserService.ts

import { inject, injectable } from 'inversify';
import { UserRepository } from '../repositories/UserRepository';

@injectable()
class UserService {
  private userRepository: UserRepository;

  constructor(@inject('UserRepository') userRepository: UserRepository) {
    this.userRepository = userRepository;
  }

  // Métodos de negócio que utilizam `userRepository`
}

export { UserService };
Enter fullscreen mode Exit fullscreen mode

repositories/UserRepository.ts

import { injectable } from 'inversify';

@injectable()
class UserRepository {
  // Métodos para acessar o banco de dados
}

export { UserRepository };
Enter fullscreen mode Exit fullscreen mode

Integrando com Express

controllers/UserController.ts

import { Request, Response } from 'express';
import { inject } from 'inversify';
import { UserService } from '../services/UserService';

class UserController {
  private userService: UserService;

  constructor(@inject('UserService') userService: UserService) {
    this.userService = userService;
  }

  public async getUser(req: Request, res: Response) {
    const user = await this.userService.getUserById(req.params.id);
    res.json(user);
  }
}

export { UserController };
Enter fullscreen mode Exit fullscreen mode

app.ts

import express from 'express';
import { container } from './inversify.config';
import { UserController } from './controllers/UserController';

const app = express();
const userController = container.resolve(UserController);

app.get('/users/:id', (req, res) => userController.getUser(req, res));

const PORT = 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Enter fullscreen mode Exit fullscreen mode

Conclusão

A utilização de IoC e DI proporciona vários benefícios em projetos Node.js com TypeScript:

  • Desacoplamento: As dependências entre as classes são minimizadas, facilitando a manutenção e o teste.
  • Flexibilidade: A substituição de implementações de dependências torna-se trivial, o que é ideal para testes e para a evolução do projeto.
  • Escalabilidade: A arquitetura da aplicação torna-se mais clara e a expansão do projeto é facilitada pela organização e modularidade.

Em suma, IoC e DI são princípios poderosos que, quando implementados corretamente, podem transformar a forma como construímos e gerenciamos aplicativos Express com TypeScript, conduzindo a um código mais limpo, testável e adaptável.

Top comments (2)

Collapse
 
lucascalazans profile image
Lucas Calazans

Muito bom, obrigado pela explicação Vitor!

Collapse
 
osergioneto profile image
Sérgio Neto

Gostei bastante mano! Isso ajuda bastante na hora de criar dependências nos testes que sejam usadas apenas nos testes