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

Vitor Rios - Jan 3 - - Dev Community

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .