Problema
Controllers inchados com SQL e regras de negócio, GlobalModule virando lixeira e testes que precisam subir app inteira. APIs “Nest” que na prática são apenas Express com decoradores.
Solução
Módulos verticais (bounded context leve): cada feature exporta apenas sua superfície. Dentro do módulo: controller fino, service com orquestração, repositório/porta para persistência. Config e cross-cutting (logger, metrics) em módulos globais mínimos.
Arquitetura
src/
common/ # pipes, filters, interceptors genéricos
config/
users/
users.module.ts
users.controller.ts
users.service.ts
persistence/
user.repository.ts
billing/
...
- DTOs +
ValidationPipena borda. - Domain errors mapeados para HTTP com exception filter.
- Filas/eventos em adapter separado do core do módulo.
Código
// users/users.service.ts
@Injectable()
export class UsersService {
constructor(private readonly repo: UserRepository) {}
async getProfile(userId: string) {
const user = await this.repo.findById(userId);
if (!user) throw new UserNotFoundException();
return UserMapper.toDto(user);
}
}// users/users.controller.ts
@Controller("users")
export class UsersController {
constructor(private readonly users: UsersService) {}
@Get(":id")
getOne(@Param("id", ParseUUIDPipe) id: string) {
return this.users.getProfile(id);
}
}Performance
Connection pool explícito; evitar N+1 com queries projetadas; cache de leitura pesada atrás de interface (ICachePort). Bull/scheduler fora do request path crítico.
Melhorias futuras
Outbox pattern para consistência eventual; OpenTelemetry; contratos versionados (/v1 estável).
Conclusão
Nest brilha quando você usa DI e módulos para isolar mudança. Isso é exatamente o que tech leads procuram em perfis backend sênior.