NestJS overhead audit
The project ran through a NestJS phase. The wins were real but narrow. The overhead was not.
NestJS has genuine strengths: a module system, first-class decorators, and a clear opinion on application structure. The question was whether those strengths justified the weight they came with. For this project, the honest answer was no.
What NestJS brought
NestJS provides a structured module system, a built-in DI container, and a decorator-driven model that maps cleanly onto familiar patterns from Angular and Spring. The opinions are well-considered and the ecosystem is mature. For teams onboarding developers at scale, the conventions are a genuine asset. Everyone lands in the same place.
Where the overhead accumulated
The DI container was the main cost. Every dependency had to be registered, decorated, and resolved through the framework machinery. Adding a new service meant touching the module definition, the provider list, and the injection tokens. Three files for what should be a one-line constructor argument. The decorator surface area grew fast, and the mental model required to reason about instantiation order was non-trivial.
| Concern | NestJS | Static factories + closures |
|---|---|---|
| Dependency wiring | DI container, decorators, modules | Explicit constructor arguments |
| New service cost | Provider registration + module update | Add parameter, done |
| Testability | TestingModule setup per test | Pass mock directly |
| Framework coupling | Deep. Decorators throughout | None in domain/application layers |
| Onboarding overhead | High. Container mental model required | Low. Plain TypeScript |
The wins were real but narrow
The structured module system did enforce boundaries. The decorator-based guard and middleware model was consistent. For a large team working on a long-lived service, those rails are worth paying for. For a single-developer project with explicit DDD boundaries already enforced by convention, the container added indirection without adding value.
What replaced it
Module classes with static factories and manual constructor injection. Each module wires its own dependencies explicitly in a composition root (`api/index.ts`). The result is plain TypeScript: no decorators, no container, no registration step. Dependencies are visible at the call site, testable by passing a mock directly, and trivially traceable through a standard IDE.
NestJS provider vs Module class with bus registration
// NestJS container registration
@Module({
providers: [UserRepository, PasswordHasher, RegisterUserHandler],
exports: [RegisterUserHandler],
})
export class IdentityModule {}
// Current. api/index.ts is the composition root
class IdentityModule {
private constructor() {}
static init(): IdentityService {
const repos = { userRepository: new UserRepository(prisma) };
commandBus.register(RegisterUserCommand, new RegisterUserHandler(repos.userRepository, ...));
return new IdentityService(commandBus, queryBus, JwtService);
}
}
const identityService = IdentityModule.init();The verdict
NestJS is not the wrong tool. It is the right tool for a different context. The overhead is justified when the team size and service complexity are high enough that conventions and the container pay for themselves. This project is not that context. Manual wiring is clearer, faster to navigate, and has zero framework coupling in the layers that matter.