Implementing Microservices with NestJS and RabbitMQ

Do you want to develop e-commerce applications that can scale and be effective? Or are you curious about how using microservices architecture can help achieve this?

In this blog post, we will discuss how to implement a microservices architecture using NestJS and RabbitMQ. The implementation will be demonstrated through the example of an e-commerce application.

Let’s dive in!

What is Microservices Architecture?

What is Microservices Architecture

The architecture of microservices is used to break down an application into smaller, independent services that can be developed, deployed, and scaled separately. 

This is done through RabbitMQ, a message broker that allows them to communicate asynchronously with each other, is loosely coupled, and resilient.


RabbitMQ Tutorial: Setting Up RabbitMQ

Setting Up RabbitMQ

  1. Sign up at CloudAMQP
  2. Log into your account
  3. Click on “Create New Instance.”
  4. Select a plan (including the free “Little Lemur” tier if applicable).
  5. Set the instance name and region.
  6. Click “Create Instance.”
  7. Open the instance and get the connection URL from the AMQP details.

Implementing Microservices with NestJS

Let’s consider an e-commerce application as an example. In this application, we’ll have multiple microservices, such as:

Implementing Microservices with NestJS

  1. Order Service: Handles order creation and management.
  2.  Notification Service: Sends notifications to users about their orders.

Each service will run on a different Nest project and communicate through RabbitMQ.

Workflow

Order Service

  • The front end sends a request to the Order Service’s /create-order endpoint.
  • Validates the request and creates an order in the database.
  • Publishes an event to the order notification queue via RabbitMQ to send an order confirmation notification.

  Notification Service

  • Listens to the order notification queue.
  • Receives the message about the new order.
  • Sends an order confirmation notification (e.g., email) to the user.

Setting Up NestJS Projects

Now let’s go through the steps for the implementation. 

Install Dependencies

First, install the necessary dependencies:

npm install @nestjs/common @nestjs/core amqplib

Generate Nest Projects

  • Generate a Nest Project with an order module with a service method (createOrder) for creating new orders.
  • Generate a Nest Project with a notification module with a service method (sendNotification) for sending notifications.

Creating the RabbitMQ Service

1.  Add a Module

  • In both projects, add a module named rabbit-mq.
  • Declare the module as @Global().

Creating the RabbitMQ Service

2. Define the RabbitMQ Service

  • Create a file named rabbit-mq.service.ts.
  • This file will contain a well-structured class, RabbitMqService, for managing RabbitMQ connections and channels using NestJS.

RabbitMQ Service Class Explanation

The RabbitMqService class handles RabbitMQ operations in a structured way. 

Here's a breakdown of its functionality:

1. Initialization

  • Connection and Channel Setup: When instantiated, the service establishes a connection and creates a channel to RabbitMQ.
  • Flexible Configuration: The connection URL is passed via the constructor, allowing easy configuration changes.
  • Initialization Method: The init method handles the setup, ensuring everything is ready to use.

2. Publish Method

  • Queue Assertion: Before sending a message, the service asserts the queue to ensure it exists.
  • Message Conversion: Messages are converted to JSON strings before being sent.

3. Subscribe Method

  • Queue Assertion: Before consuming messages, the service asserts the queue.
  • Message Handling: Received messages are parsed from JSON and passed to a callback function.

4. Initialization Check

  • Ensure Initialization: The ensureInitialized method checks if the service is ready, waiting for the init method to complete if not.

5. Graceful Shutdown

  • Cleanup: The onModuleDestroy method closes the RabbitMQ connection and channel when the service is destroyed, ensuring a clean shutdown.

RabbitMQ Service Code

import { Injectable, OnModuleDestroy } from '@nestjs/common';

import { connect, Channel, Connection } from 'amqplib';



@Injectable()

export class RabbitMqService implements OnModuleDestroy {

  private connection: Connection;

  private channel: Channel;

  private initialized = false;

  private initPromise: Promise<void>;



  constructor(private readonly connectionUrl: string) {

    this.initPromise = this.init();

  }



  private async init() {

    try {

      if (this.connection && this.channel) {

        console.log('RabbitMq connection already established.');

        return;

      }

      this.connection = await connect(this.connectionUrl);

      this.channel = await this.connection.createChannel();

      this.initialized = true;

      console.log('RabbitMq connection established.');

    } catch (error) {

      console.error('Failed to establish RabbitMq connection:', error);

      throw error; // Consider implementing a retry strategy here.

    }

  }



  async publish(queue: string, message: any) {

    await this.ensureInitialized();

    try {

      await this.channel.assertQueue(queue);

      this.channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)));

    } catch (error) {

      console.error(`Failed to publish message to queue ${queue}:`, error);

    }

  }



  async subscribe(queue: string, callback: (message: any) => void) {

    await this.ensureInitialized();

    try {

      await this.channel.assertQueue(queue);

      this.channel.consume(queue, (msg) => {

        if (msg !== null) {

          callback(JSON.parse(msg.content.toString()));

          this.channel.ack(msg);

        }

      });

      console.log(`Successfully subscribed to queue: ${queue}`);

    } catch (error) {

      console.error(`Failed to subscribe to queue ${queue}:`, error);

    }

  }



  private async ensureInitialized() {

    if (!this.initialized) {

      await this.initPromise;

    }

  }



  async onModuleDestroy() {

    try {

      await this.channel.close();

      await this.connection.close();

      console.log('RabbitMq connection closed.');

    } catch (error) {

      console.error('Failed to close RabbitMq connection:', error);

    }

  }

}

Data Sharing By RabbitMq Service 

Now, we will share data between order and notification microservices through the RabbitMQ service. 

Data Sharing By RabbitMq Service

In our example, the data flows from the order service to the notification service. Hence, the notification service that is receiving data will have to subscribe to a queue on which data will be published by the other services (i.e. order service).

Subscribe Queue

Here, when the notification service module is initialized, it will subscribe to a queue ‘order-notification’ and bind a method ‘sendNotification’ with that.

Means, that whenever data is published to the ‘order-notification’ queue, the notification service will receive the data and will call the ‘send notification’ method to take an action (i.e. send a notification to the user mentioned in the data published to the queue)

import { Injectable } from '@nestjs/common';
import { RabbitMqService } from 'src/rabbit-mq/rabbit-mq.service';

@Injectable()
export class NotificationService {
 constructor(private rabbitMQService: RabbitMqService) {}

 async onModuleInit() {
   this.rabbitMQService.subscribe(
     'order-notification',
     this.sendNotification.bind(this),
   );
 }

 async sendNotification(order: object) {
   // Logic to send notification based on the order details
   console.log('🔔 sendNotification is called...');
 }
}

Publish Data

After an order is created in the order service, it will share data to the notification service by publishing the required data to the ‘order notification’.

import { Body, Controller, Post } from '@nestjs/common';
import { OrderService } from './order.service';
import { RabbitMqService } from 'src/rabbit-mq/rabbit-mq.service';

@Controller('order')
export class OrderController {
 constructor(
   private orderService: OrderService,
   private rabbitMQService: RabbitMqService,
 ) {}

 @Post('/new-order')
 createOrder(@Body() body) {
   // < Logic to to create order >
   console.log('Order created successfully...');

   // -> send data to notification service
   this.rabbitMQService.publish('order-notification', body);

   return 'order placed';
 }
}

Conclusion

By utilizing NestJS and RabbitMQ, microservices architecture is established leading to the growth, efficacy, and flexibility of your applications. With the help of this comprehensive guide, you will be able to create a durable and expandible online shopping application.

Regardless of if you are a novice or an expert coder; this manual will assist you in understanding and using microservices in your work.

Happy coding!

Tags:

Microservices

Development

RabbitMQ

Ecommerce Development

Jay Jethava
Jay Jethava

Node Developer

Support

Frequently Asked Questions

TST Technology FAQ

NestJS is better than Express because of its more structured and modular methodology that helps in microservices communication and scalability, making it the better alternative for the implementation of microservices.

Deciding when to use Kafka vs RabbitMQ depends on your requirements. Kafka is ideal for real-time streaming of data and high-throughput scenarios. RabbitMQ is more suitable for complex routing and guaranteed message delivery, making it an excellent choice for NestJS microservices in an e-commerce application.

To implement microservices in NestJS, break your application into smaller, independent services. Each service will be its own NestJS project. Use a message broker like RabbitMQ for communication between these services. Create a service in one project to handle a specific task, like order management, and another service in another project for notifications. The services will communicate by sending messages to each other through RabbitMQ queues.

To use RabbitMQ in microservices, first set up RabbitMQ on a service like CloudAMQP. Then, in your NestJS projects, install the amqplib package to connect to RabbitMQ. Create a service that can publish messages to a queue and another service to subscribe to that queue and handle the messages. This allows different microservices to communicate asynchronously.

NestJS architecture is modular and built on controllers, providers, and modules. Controllers handle incoming requests and return responses. Providers, like services, handle the business logic. Modules organize the application structure, grouping related controllers and providers. This architecture makes it easy to create and manage large-scale applications.

RabbitMQ is written in Erlang. However, it can be used with many programming languages like JavaScript, Python, and Java through client libraries.

Yes, RabbitMQ has an HTTP-based management API. It enables you to manage and monitor your RabbitMQ server including queues, exchanges as well as bindings.

In Node.js, to smoothly integrate microservices you need a framework like NestJS with built-in support for them. Additionally, set up a messaging broker such as RabbitMQ for purposes of communication. Create separate Node.js applications for each microservice. Each service will handle its specific task and communicate with other services through RabbitMQ, ensuring they work together efficiently.

Watch These Videos for More Insights