DEV Community

Cover image for Design Patterns in NestJS
Geampiere Jaramillo
Geampiere Jaramillo

Posted on

Design Patterns in NestJS

When developing backend applications with NestJS, one of the biggest challenges is creating an architecture that is scalable, maintainable, and extensible. One effective way to achieve this is by using design patterns, which allow us to build more flexible, decoupled, and reusable solutions.

We will explore the most common design patterns you can implement in your NestJS applications, such as Dependency Injection, Singleton, Decorators, Strategy, and others. You will learn how to apply these patterns to improve code quality and long-term maintainability.

1. Dependency Injection (DI)

Dependency Injection is one of the core principles of NestJS. This pattern allows classes to depend on other objects without creating them explicitly, which facilitates decoupling and component reusability.

How it works in NestJS:
NestJS uses TypeScript's dependency injection container. Whenever you inject a service into another via the constructor, NestJS handles the instantiation and passes the dependency.

@Injectable()
export class MyService {
  getHello(): string {
    return 'Hello, World!';
  }
}

@Controller()
export class MyController {
  constructor(private readonly myService: MyService) {}

  @Get()
  getHello(): string {
    return this.myService.getHello();
  }
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Decoupling: Classes do not need to know the implementation of their dependencies.
  • Scalability: Easy to scale and maintain when adding new dependencies.
  • Testing: Enables mocking services during unit testing.

2. Singleton

The Singleton pattern ensures a class has only one instance and provides a global point of access. NestJS uses this by default for services.

@Injectable()
export class MySingletonService {
  private counter = 0;

  increment() {
    this.counter++;
    return this.counter;
  }

  getCounter() {
    return this.counter;
  }
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Consistency: Maintains shared state throughout the application.
  • Resource Management: Ideal for managing resources like database connections.

3. Decorator Pattern

Decorators are fundamental in NestJS. They allow you to add functionality to classes and methods without modifying their structure.

@Controller('users')
export class UserController {
  @Get(':id')
  getUser(@Param('id') id: string): string {
    return `User ID: ${id}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Readability: Decorators make code more readable and organized.
  • Flexibility: Adds functionality in a modular way.

4. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It is useful when you need different behaviors for a service.

interface PaymentStrategy {
  pay(amount: number): string;
}

@Injectable()
export class CreditCardPayment implements PaymentStrategy {
  pay(amount: number): string {
    return `Paid ${amount} using Credit Card`;
  }
}

@Injectable()
export class PayPalPayment implements PaymentStrategy {
  pay(amount: number): string {
    return `Paid ${amount} using PayPal`;
  }
}

@Injectable()
export class PaymentService {
  constructor(private readonly paymentStrategy: PaymentStrategy) {}

  processPayment(amount: number): string {
    return this.paymentStrategy.pay(amount);
  }
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Flexibility: Easily change system behavior without altering the structure.
  • Extensibility: New strategies can be added without affecting existing code.

5. Observer Pattern

The Observer pattern is useful when multiple objects need to be notified of changes in another object's state. In NestJS, this can be implemented using events.

@Injectable()
export class MyService {
  private eventEmitter = new EventEmitter();

  constructor() {
    this.eventEmitter.on('userCreated', (user) => {
      console.log('User Created:', user);
    });
  }

  createUser(user: any) {
    this.eventEmitter.emit('userCreated', user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Decoupling: Observers react to events without needing to know the subject's internal logic.
  • Scalability: Easy to add new functionalities by subscribing to events.

6. Command Pattern

The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests.

class CreateUserCommand {
  constructor(public readonly username: string, public readonly email: string) {}
}

@Injectable()
export class UserService {
  executeCreateUserCommand(command: CreateUserCommand): void {
    console.log(`Creating user with username: ${command.username} and email: ${command.email}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Decoupling: Separates the request from its execution.
  • Reusability: Commands can be reused in different parts of the app.

Conclusion

Design patterns are powerful tools for building scalable and maintainable backend applications. In NestJS, you can take full advantage of these patterns to structure your code in a modular, decoupled, and efficient way.

The key is to understand when and how to apply each pattern based on your project’s requirements. Start experimenting with them and see how your NestJS architecture improves.

Top comments (0)