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?
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
- Sign up at CloudAMQP
- Log into your account
- Click on “Create New Instance.”
- Select a plan (including the free “Little Lemur” tier if applicable).
- Set the instance name and region.
- Click “Create Instance.”
- 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:
- Order Service: Handles order creation and management.
- 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().
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.
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!