NestJS Modularity: Learning from a Reverted TypeORM and Service Export
The SBSofiaBartoli/ecommerce-md-back project is an e-commerce backend system. We recently encountered a common challenge related to module design and dependency management when implementing features.
The Situation
During a recent development cycle for our e-commerce backend, a new feature involving user management required access to the UsersService and certain TypeORM repository instances. To facilitate this, a decision was made to export TypeORM providers and the UsersService directly from the UsersModule. The thinking was straightforward: make these components readily available for any other module that needed them.
The Descent
However, this approach quickly revealed subtle but significant drawbacks. While initially appearing to simplify access, exporting core components like TypeORM providers and primary services from a feature module introduced a tighter coupling than anticipated. We observed a potential for circular dependencies, making the module harder to test in isolation. It also blurred the lines of responsibility; other modules began relying on UsersModule for generic database access, which wasn't its primary purpose. This increased complexity and reduced the maintainability of our module architecture.
The Wake-Up Call
The issues, though not immediately blocking, signaled a deviation from good modular design principles. NestJS is built on a powerful Dependency Injection system and a clear module structure precisely to avoid these kinds of pitfalls. Realizing that the direct export of TypeORM and UsersService was creating a less robust and scalable system, the decision was made to revert the changes. This allowed us to step back and re-evaluate our approach to module encapsulation.
The Technical Lesson
The revert prompted a deeper look into NestJS's philosophy on modularity and dependency management. The key lesson here is about judicious use of the exports array in the @Module decorator.
-
TypeORM and Repository Pattern: For
TypeORM, the best practice is to useTypeOrmModule.forFeature()within the specific module where the entities are owned and the repositories are used. These repositories are then injected directly into the services within that module. Other modules should not typically importTypeORMproviders directly from a feature module. If cross-module database interaction is needed, create a dedicated service in the owning module that exposes only the necessary methods, not the raw repository.// users.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; @Module({ imports: [TypeOrmModule.forFeature([User])], // User entity and its repository available here providers: [UsersService], controllers: [UsersController], // No export of TypeOrmModule or raw repository here exports: [UsersService] // Export UsersService ONLY if other modules genuinely need to use its public methods }) export class UsersModule {} -
Service Encapsulation: Services like
UsersServiceshould be declared asproviderswithin their respective module. They should only beexportedif their public methods are truly intended to be consumed by other, unrelated modules. Over-exporting services makes refactoring harder and can lead to unintended dependencies. TheUsersServiceitself should encapsulate the logic for interacting with theUserRepository.// users.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './entities/user.entity'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>, ) {} async findOne(id: number): Promise<User | undefined> { return this.usersRepository.findOneBy({ id }); } // ... other user-related methods }
By reverting the broad exports, we restored the principle of least exposure, ensuring that each module only exposes what is strictly necessary, thereby improving maintainability, testability, and overall architectural clarity.
The Takeaway
When working with NestJS, meticulously consider what you export from your modules. Broadly exporting core services or TypeORM providers can lead to unforeseen coupling and hinder modularity. Instead, leverage NestJS's Dependency Injection system to inject services and repositories where they are needed internally within a module, and only export a service's public API when genuinely required by other modules. Embrace encapsulation to build more resilient and maintainable applications.
Generated with Gitvlg.com