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:
- Importar a classe
- Adicionar em
providers - Declarar dependências no
inject - Escrever uma factory
- Exportar se outro módulo precisasse
- 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.