Microservices with NestJS

Introduction to microservices architecture and how to build microservices using NestJS with gRPC or message queues (e.g., RabbitMQ, Kafka).


Message Queues (RabbitMQ/Kafka) with NestJS

Introduction to Message Queues

Message queues are a crucial component in building scalable and resilient distributed systems. They enable asynchronous communication between different services or applications. Instead of direct communication (like HTTP calls), services communicate by publishing messages to a queue, and other services consume those messages from the queue. This decouples services, making them more independent and robust.

Key benefits of using message queues:

  • Decoupling: Services don't need to know about each other directly.
  • Asynchronous Communication: Services can continue operating even if other services are temporarily unavailable.
  • Scalability: Consumers can scale independently to handle varying workloads.
  • Reliability: Messages are persisted in the queue until they are successfully processed.
  • Flexibility: You can easily add or remove consumers without affecting the producers.

RabbitMQ

RabbitMQ is a popular, open-source message broker that implements the Advanced Message Queuing Protocol (AMQP). It's known for its flexibility, ease of use, and wide range of features.

Key characteristics of RabbitMQ:

  • AMQP: Adheres to the AMQP standard.
  • Routing: Provides various exchange types (direct, fanout, topic, headers) for flexible message routing.
  • Guaranteed Delivery: Supports message persistence and acknowledgements to ensure messages are delivered.
  • Clustering: Can be clustered for high availability and scalability.

Kafka

Kafka is a distributed streaming platform that's designed for high-throughput, fault-tolerant data pipelines and streaming analytics. It's often used for collecting and processing real-time data streams.

Key characteristics of Kafka:

  • High Throughput: Designed for handling large volumes of data.
  • Scalability: Can be scaled horizontally by adding more brokers to the cluster.
  • Persistence: Stores messages on disk for a configurable amount of time.
  • Fault Tolerance: Replicates data across multiple brokers to ensure data availability.
  • Publish-Subscribe: Uses a publish-subscribe model for distributing messages to consumers.

Configuring NestJS to Connect to a Message Queue

NestJS provides a powerful microservices module that makes it easy to connect to and interact with message queues like RabbitMQ and Kafka. Here's how to configure NestJS for each:

RabbitMQ Configuration

First, install the necessary package:

npm install --save @nestjs/microservices amqplib

Then, configure the RabbitMQ client in your NestJS module (e.g., AppModule):

 import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'MATH_SERVICE', // A unique name for your microservice client
        transport: Transport.RMQ,
        options: {
          urls: ['amqp://localhost:5672'], // RabbitMQ connection URL
          queue: 'math_queue', // The queue you want to use
          queueOptions: {
            durable: false // Whether the queue should survive server restarts
          },
        },
      },
    ]),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {} 

Kafka Configuration

Install the necessary packages:

npm install --save @nestjs/microservices kafkajs

Configure the Kafka client in your NestJS module:

 import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'HERO_SERVICE', // A unique name for your microservice client
        transport: Transport.KAFKA,
        options: {
          client: {
            clientId: 'hero-service',
            brokers: ['localhost:9092'], // Kafka broker addresses
          },
          consumer: {
            groupId: 'hero-consumer', // Consumer group ID
          },
        },
      },
    ]),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {} 

Publishing and Consuming Messages Using NestJS's Microservices Module

Publishing Messages

To publish messages, inject the client you configured in your module and use the emit method (for fire-and-forget) or the send method (for request-response):

 import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

@Injectable()
export class AppService {
  constructor(@Inject('MATH_SERVICE') private readonly client: ClientProxy) {}

  async getHello(): Promise<string> {
    // Fire-and-forget (no response expected)
    this.client.emit('math.sum', [1, 2, 3]);

    // Request-response (expect a result)
    const result = await this.client.send('math.calculate', { numbers: [4, 5, 6], operation: '+' }).toPromise();
    return `Result from math service: ${result}`;
  }
} 

Consuming Messages

To consume messages, use the @MessagePattern decorator in your controller:

 import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class AppController {
  @MessagePattern('math.sum') // Listens for messages on the 'math.sum' pattern
  async accumulate(data: number[]): Promise<number> {
    console.log('Received data:', data);
    return (data || []).reduce((a, b) => a + b);
  }

  @MessagePattern('math.calculate')
  async calculate(data: { numbers: number[], operation: string }): Promise<number> {
     console.log('Received calculation request:', data);

     if (!data || !data.numbers || !data.operation) {
          return NaN; // or throw an error
     }

      switch (data.operation) {
          case '+':
              return data.numbers.reduce((a, b) => a + b, 0);
          case '-':
              return data.numbers.reduce((a, b) => a - b);
          case '*':
              return data.numbers.reduce((a, b) => a * b, 1);
          case '/':
              if (data.numbers.includes(0)) {
                  return NaN; // or throw an error for division by zero
              }
              return data.numbers.reduce((a, b) => a / b);
          default:
              return NaN; // or throw an error for invalid operation
      }

  }
} 

When a message matching the pattern is received, the decorated method will be executed.