DEV Community

Cover image for πŸš€ Applying SOLID Principles in NestJS: A Practical Guide
Abhinav
Abhinav

Posted on

πŸš€ Applying SOLID Principles in NestJS: A Practical Guide

When building robust and maintainable applications with NestJS, the SOLID principles offer a proven path to writing clean and scalable code. Each principleβ€”Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversionβ€”guides how to design your application for simplicity and extensibility.

Let’s dive into these principles with examples and flowcharts! 🌟


πŸͺ© 1. Single Responsibility Principle (SRP)

A class should have one and only one reason to change.

In NestJS, separation of concerns is achieved by dividing responsibilities across controllers, services, and repositories.

Example:

// 🍴 MealController.ts
@Controller('meals')
export class MealController {
  constructor(private readonly mealService: MealService) {}

  @Post()
  create(@Body() createMealDto: CreateMealDto) {
    return this.mealService.create(createMealDto);
  }
}

// πŸ› οΈ MealService.ts
@Injectable()
export class MealService {
  create(createMealDto: CreateMealDto) {
    // Business logic for creating a meal
  }
}
Enter fullscreen mode Exit fullscreen mode

By keeping these responsibilities separate, changes to one layer don’t affect others. 🎯


🎭 2. Open/Closed Principle (OCP)

A class should be open for extension but closed for modification.

In NestJS, decorators, inheritance, and abstraction help us extend functionality without modifying existing code.

Example:

// πŸ“‹ LoggerService.ts
@Injectable()
export class LoggerService {
  log(message: string) {
    console.log(`Log: ${message}`);
  }
}

// πŸ”§ CustomLoggerService.ts
@Injectable()
export class CustomLoggerService extends LoggerService {
  log(message: string) {
    super.log(message);
    console.log(`Custom Log: ${message}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

This way, LoggerService remains untouched, and custom behavior is added via CustomLoggerService. πŸš€


🀝 3. Liskov Substitution Principle (LSP)

Subtypes must be replaceable with their base types without affecting functionality.

By leveraging interfaces, NestJS allows services to be swapped seamlessly. This is especially useful for testing.

Example:

// πŸ€“ MealServiceInterface.ts
export interface MealServiceInterface {
  create(createMealDto: CreateMealDto): any;
}

// βœ… MealService.ts
@Injectable()
export class MealService implements MealServiceInterface {
  create(createMealDto: CreateMealDto) {
    // Business logic
  }
}

// πŸ§ͺ MockMealService.ts
@Injectable()
export class MockMealService implements MealServiceInterface {
  create(createMealDto: CreateMealDto) {
    // Mock logic for tests
  }
}
Enter fullscreen mode Exit fullscreen mode

In this setup, you can replace MealService with MockMealService during testing without breaking the app. βœ…


🏠 4. Interface Segregation Principle (ISP)

Classes should not be forced to implement methods they don’t use.

Split large interfaces into smaller, focused ones to avoid bloated implementations.

Example:

// πŸ› οΈ DatabaseReadable.ts
export interface DatabaseReadable {
  findAll(): any[];
  findOne(id: number): any;
}

// πŸ› οΈ DatabaseWritable.ts
export interface DatabaseWritable {
  create(data: any): any;
  update(id: number, data: any): any;
  delete(id: number): void;
}

// πŸ“ MealRepository.ts
export class MealRepository implements DatabaseReadable, DatabaseWritable {
  findAll() { /* ... */ }
  findOne(id: number) { /* ... */ }
  create(data: any) { /* ... */ }
  update(id: number, data: any) { /* ... */ }
  delete(id: number) { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Each interface focuses on specific responsibilities, keeping the code clean and modular. πŸ› οΈ


πŸ”— 5. Dependency Inversion Principle (DIP)

High-level modules should depend on abstractions, not on concrete implementations.

In NestJS, dependency injection simplifies this by decoupling classes from their dependencies.

Example:

// πŸ“œ MealServiceInterface.ts
export abstract class MealServiceInterface {
  abstract create(createMealDto: CreateMealDto): any;
}

// πŸ› οΈ MealService.ts
@Injectable()
export class MealService extends MealServiceInterface {
  create(createMealDto: CreateMealDto) {
    // Business logic
  }
}

// πŸš€ MealController.ts
@Controller('meals')
export class MealController {
  constructor(private readonly mealService: MealServiceInterface) {}

  @Post()
  create(@Body() createMealDto: CreateMealDto) {
    return this.mealService.create(createMealDto);
  }
}
Enter fullscreen mode Exit fullscreen mode


This allows us to switch to a different implementation of MealServiceInterface without altering the controller. πŸ”„


🍝 SOLID Principles in a Diet Plan Service

Recently, I have been working on a diet plan service in NestJS. Here's how the SOLID principles help:

  • SRP:

    • Controllers handle HTTP endpoints like adding meals or viewing plans.
    • Services manage business logic, such as calculating calories or meal macros.
    • Repositories interact with the database for storing and retrieving diet data.
  • OCP: New diet plans like KetoDietPlanService or VeganDietPlanService can extend a base DietPlanService without modifying its core logic.

  • LSP: Replace the actual DietPlanService with a MockDietPlanService during testing to validate APIs without relying on the database.

  • ISP: Split interfaces for user management, meal tracking, and dietary calculations to keep concerns modular.

  • DIP: Controllers rely on abstractions like DietPlanInterface, making it easier to introduce alternative implementations for new diet strategies.


πŸŽ‰ Why SOLID Matters in NestJS

Adopting the SOLID principles in NestJS ensures:

  • πŸ› οΈ Maintainability: Easy to understand and modify code.
  • 🌱 Scalability: Add new features without breaking existing functionality.
  • πŸ§ͺ Testability: Mock dependencies for seamless testing.

🌟 Final Thoughts

By applying these principles, our NestJS projects become more organized, extensible, and robust. Whether you’re building small-scale apps or enterprise-level solutions, keeping your code SOLID will save time and effort in the long run.

πŸ’‘ Let’s keep our projects clean, modular, and scalable! Happy coding! πŸŽ‰

Top comments (0)