Back to all posts

Writing Modular and Reusable Code in NestJS

S
sonhp
10 min read

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, only EmailService 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 swap EmailService 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 a BaseRepository that implements IRepository<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!

S

Written by sonhp

Technical writer and developer passionate about web technologies.

Related Articles