Executando verificação de segurança...
1

Como simplifiquei meus módulos no NestJS e eliminei boilerplate

Você já abriu um arquivo de módulo e ficou cansado só de bater o olho?

O Problema

Esse é um exemplo real de um módulo em um projeto com NestJS em que eu estava trabalhando:

import { Module } from '@nestjs/common';
import { LoggerService } from './logger/logger.service';
import { TokenService } from './token/token.service';
import { PaymentRepository } from './repositories/payment.repository';
import { OrderRepository } from './repositories/order.repository';
import { PaymentService } from './services/payment.service';
import { OrderService } from './services/order.service';

@Module({
  providers: [
    {
      provide: LoggerService,
      useFactory: () => new LoggerService('APP'),
    },
    {
      provide: TokenService,
      useFactory: () => new TokenService('my-secret-key'),
    },
    {
      provide: PaymentRepository,
      useClass: PaymentRepository,
    },
    {
      provide: OrderRepository,
      useClass: OrderRepository,
    },
    {
      provide: PaymentService,
      inject: [LoggerService, TokenService, PaymentRepository],
      useFactory: (logger: LoggerService, token: TokenService, repo: PaymentRepository) => {
        return new PaymentService(logger, token, repo);
      },
    },
    {
      provide: OrderService,
      inject: [LoggerService, OrderRepository, PaymentService],
      useFactory: (logger: LoggerService, orderRepo: OrderRepository, paymentService: PaymentService) => {
        return new OrderService(logger, orderRepo, paymentService);
      },
    },
  ],
  exports: [PaymentService, OrderService],
})
export class AppModule {}

Isso é um único módulo. Seis providers. E, sinceramente, para entender o que está acontecendo, você precisa ler linha por linha.

Por que isso é ruim

Alguns pontos que começaram a me incomodar bastante:

É repetitivo.
Todo service segue o mesmo padrão: provide, inject, useFactory. Você escreve o mesmo boilerplate várias vezes. Adicionou uma dependência? Atualiza o array de inject e os parâmetros da factory. Esqueceu um? Erro em runtime.

É difícil de ler.
Quer saber do que o OrderService depende? Precisa achar o bloco do provider e interpretar o array de inject. Agora imagina isso com 20 providers. Escala mal.

É tudo misturado.
Configuração, dependências e instanciação ficam todas dentro do módulo. Não existe um ponto único onde você consegue enxergar como a aplicação está conectada.

Não escala bem.
Conforme o projeto cresce, os módulos viram arquivos enormes. Infra compartilhada (banco, logger, etc.) começa a se repetir em vários módulos. O AppModule vira um festival de imports com 100 linhas.

Toda vez que eu criava um novo service, eu repetia o ritual:

  1. Importar a classe
  2. Adicionar em providers
  3. Declarar dependências no inject
  4. Escrever uma factory
  5. Exportar se outro módulo precisasse
  6. Repetir em outros módulos

Eu só queria escrever:

new SomeService(depA, depB)

e seguir com o resto do sistema.


Uma abordagem mais limpa com nestjs-moduly

O nestjs-moduly propõe outro caminho. Em vez de configurar providers dentro dos módulos, você declara as instâncias em um único lugar central.

// instances.ts
import { createInstanceGroup } from 'nestjs-moduly';

export const Infrastructure = createInstanceGroup('Infrastructure');
export const Repository = createInstanceGroup('Repository');
export const Service = createInstanceGroup('Service');

// Singletons com configuração
Infrastructure.Logger = new LoggerService('APP');
Infrastructure.Token = new TokenService('my-secret-key');

// Repositórios
Repository.Payment = new PaymentRepository();
Repository.Order = new OrderRepository();

// Services - dependências direto no construtor
Service.Payment = new PaymentService(
  Infrastructure.Logger,
  Infrastructure.Token,
  Repository.Payment,
);

Service.Order = new OrderService(
  Infrastructure.Logger,
  Repository.Order,
  Service.Payment,
);

Agora o módulo fica assim:

// app.module.ts
import { Infrastructure, Repository, Service } from './instances';

@Module({
  imports: [
    Infrastructure.Logger,
    Infrastructure.Token,
    Repository.Payment,
    Repository.Order,
    Service.Payment,
    Service.Order,
  ],
  controllers: [OrderController, PaymentController],
})
export class AppModule {}

Sem factory. Sem array de inject. Apenas uma lista clara do que o módulo usa.


Por que isso funciona melhor

Menos configuração, mais código real.
Você escreve TypeScript de verdade — new Service(dep) — em vez de objetos de configuração. O imports do módulo já deixa explícito do que ele depende.

Instâncias centralizadas.
Services, repositórios e infraestrutura ficam todos declarados em um único arquivo. Mudou configuração de banco? Um lugar só. Quer ver as dependências de OrderService? Abre o instances.ts.

Refatoração simples.
Mover um service de módulo? Ajusta os imports. Não precisa reescrever provider.

Escala de forma previsível.
Conforme a aplicação cresce, o instances.ts cresce linearmente. Os módulos continuam enxutos.

Grafo de dependência explícito.

Service.Order
   ├── Infrastructure.Logger
   ├── Repository.Order
   └── Service.Payment

Você enxerga as dependências direto no construtor. Não precisa sair caçando inject espalhado pelo módulo.


Como usar

1. Instale o pacote

npm install nestjs-moduly

2. Crie os grupos de instâncias

// src/instances.ts
import { createInstanceGroup } from 'nestjs-moduly';

export const Database = createInstanceGroup('Database');
export const Repository = createInstanceGroup('Repository');
export const Service = createInstanceGroup('Service');

O nome passado ('Database', 'Repository', etc.) vira o prefixo do injection token — útil caso você precise injetar por token em vez da classe.


3. Atribua as instâncias

import { DatabaseService } from './services/database.service';
import { UserRepository } from './repositories/user.repository';
import { UserService } from './services/user.service';

Database.Primary = new DatabaseService({ host: 'localhost', port: 5432 });
Repository.Users = new UserRepository(Database.Primary);
Service.User = new UserService(Repository.Users);

Repare que UserRepository recebe Database.Primary direto no construtor. São instâncias reais, não configurações de provider. É TypeScript puro.

O singleton é criado uma vez aqui e compartilhado onde for importado.


4. Importe nos módulos

import { Module } from '@nestjs/common';
import { Database, Repository, Service } from './instances';
import { UserController } from './controllers/user.controller';

@Module({
  imports: [
    Database.Primary,
    Repository.Users,
    Service.User,
  ],
  controllers: [UserController],
})
export class AppModule {}

Cada instância importada disponibiliza o singleton para os controllers e providers do módulo.


5. Injete normalmente

Nada muda nos controllers:

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}
  
  @Get()
  findAll() {
    return this.userService.findAll();
  }
}

Sem decorators especiais. A lib registra cada instância usando a própria classe como token, então a injeção padrão funciona direto.


Fim de useFactory. Fim de inject. Fim de “provider soup”.

Repositório no GitHub:
https://github.com/VictorlBueno/nestjs-moduly

Como você lida com seus modules? Não sei se é minha bolha ou se existem poucas abordagens para lidar com module.

Carregando publicação patrocinada...