Writing Clean and Maintainable Code in NestJS
Part 3 of 12
Writing Modular and Reusable Code in NestJS
One of the biggest challenges in software development is keeping code modular and reusable.
Modular code is:
✅ Easier to maintain – Changes in one module don’t break others.
✅ More reusable – Components can be used in multiple places.
✅ Easier to test – Small, independent modules are simpler to test.
In this post, we’ll cover how to design modular and reusable code in NestJS.
1. Breaking Down Features into Modules
In NestJS, a module is a self-contained unit that groups related functionality.
✅ Example: Feature-Based Modules
src/
├── modules/
│ ├── users/
│ │ ├── users.controller.ts
│ │ ├── users.service.ts
│ │ ├── users.module.ts
│ ├── orders/
│ │ ├── orders.controller.ts
│ │ ├── orders.service.ts
│ │ ├── orders.module.ts
├── common/
│ ├── services/
│ │ ├── email.service.ts
│ │ ├── logging.service.ts
Each module handles only one responsibility.
Rule of Thumb: If a module has too many responsibilities, break it into smaller modules.
2. Creating Reusable Services
A reusable service is decoupled from specific modules so it can be used anywhere.
✅ Example: Email Service (Reusable)
@Injectable()
export class EmailService {
sendEmail(to: string, subject: string, body: string) {
console.log(`Sending email to ${to} with subject: ${subject}`);
}
}
✅ Injecting Email Service in Multiple Modules
@Injectable()
export class UsersService {
constructor(private readonly emailService: EmailService) {}
async registerUser(email: string) {
this.emailService.sendEmail(email, 'Welcome!', 'Thank you for signing up!');
}
}
@Injectable()
export class OrdersService {
constructor(private readonly emailService: EmailService) {}
async sendOrderConfirmation(email: string) {
this.emailService.sendEmail(
email,
'Order Confirmation',
'Your order has been placed.',
);
}
}
Why?
✅ Reduces duplication – No need to rewrite email logic in every module.
✅ Encapsulates logic – If the email implementation changes, onlyEmailService
needs updating.
3. Using Dependency Injection for Loose Coupling
Hardcoded dependencies make testing and refactoring difficult. Instead, use dependency injection.
❌ Bad Example (Tightly Coupled)
export class UsersService {
private emailService = new EmailService();
registerUser(email: string) {
this.emailService.sendEmail(email, 'Welcome!', 'Thank you for signing up!');
}
}
✅ Good Example (Using Dependency Injection)
@Injectable()
export class UsersService {
constructor(private readonly emailService: EmailService) {}
registerUser(email: string) {
this.emailService.sendEmail(email, 'Welcome!', 'Thank you for signing up!');
}
}
Why?
✅ Easier to replace dependencies – You can swapEmailService
with a mock in tests.
✅ Follows NestJS best practices – Services are injected automatically.
4. Creating Global Modules for Shared Functionality
Instead of importing shared services in every module, use a global module.
✅ Create a SharedModule
@Module({
providers: [EmailService, LoggingService],
exports: [EmailService, LoggingService],
})
export class SharedModule {}
✅ Import SharedModule
Globally
@Module({
imports: [SharedModule],
})
export class AppModule {}
Now, EmailService
and LoggingService
are available everywhere without re-importing.
Why?
✅ Reduces redundant imports
✅ Keeps the codebase cleaner
5. Using Interfaces for Better Reusability
Interfaces help standardize contracts between modules.
✅ Example: Generic Repository Interface
export interface IRepository<T> {
findById(id: number): Promise<T>;
save(entity: T): Promise<T>;
}
✅ Example: Implementing Repository for Users
@Injectable()
export class UsersRepository implements IRepository<User> {
async findById(id: number): Promise<User> {
return await this.userRepo.findOne({ where: { id } });
}
async save(user: User): Promise<User> {
return await this.userRepo.save(user);
}
}
Why?
✅ Enforces consistency – All repositories follow the same structure.
✅ Increases reusability – You can create aBaseRepository
that implementsIRepository<T>
.
6. Writing Utility Functions for Repeated Logic
If you repeat the same logic across multiple services, move it to a utility function.
✅ Example: Utility Function for Formatting Names
export function formatName(name: string): string {
return name.trim().toLowerCase();
}
✅ Use in Services
const formattedName = formatName(user.name);
Why?
✅ Keeps services focused on business logic
✅ Avoids code duplication
7. Summary
To write modular and reusable code in NestJS:
✅ Break down features into modules to keep code organized.
✅ Create reusable services to avoid duplication.
✅ Use dependency injection for loose coupling.
✅ Use global modules for shared functionality.
✅ Use interfaces to enforce consistency in repositories.
✅ Extract utility functions for commonly used logic.
In the next post, we’ll dive into error handling and logging best practices in NestJS.
How do you structure your NestJS modules? Let’s discuss in the comments!