DEV Community

Mouloud hasrane
Mouloud hasrane

Posted on

Mastering Microservices with NestJS: A Practical Guide to Scalable Architectures

Ever wondered how tech giants like Netflix, Uber, and Amazon handle millions of requests without breaking a sweat? The secret lies in microservices architecture—a game-changing approach that makes applications more scalable, resilient, and easier to maintain.

In this guide, we'll demystify microservices, explore their benefits, and walk through a hands-on implementation using NestJS and RabbitMQ. Ready to level up your backend skills? Let’s dive in! 🚀

📌 Why NestJS?

One of the standout features of NestJS is its native support for microservices architecture. When using microservices in NestJS, you can still leverage decorators, guards, pipes, interceptors, and other features found in a traditional monolithic NestJS app.

Before we jump into the code, let’s break down what microservices are and why they are so powerful.

🏗️ What Are Microservices?

In a typical monolithic architecture, a single server handles all requests, processes the logic, and returns a response.

In contrast, a microservices architecture breaks down an application into multiple independent services, each handling a specific piece of logic.

Instead of one big server doing everything, we have multiple smaller services (often called microservices) working together.
Enter fullscreen mode Exit fullscreen mode

🛠️ Why Microservices?

✅ Separation of Concerns & Decoupling:

Each microservice is responsible for a specific aspect of the application, making it easier to scale and debug.

✅ Fault Tolerance:

If one service goes down, only that part of the app is affected, not the entire system.

✅ Scalability & Performance:

Since services are independent, they can be scaled individually based on demand.

Now after that, you might Ask : Now, you might ask: how do these services communicate with each other?

Ande we need to introduce the concept of a transport layer, you can imagine it for now as the route or the bridge that links all of the services together it sends data across multiple services :fmo us transport layers are :

  • TCP
  • RMQ
  • BullMq
  • Grpc and a lot more.

Communication Example

What is cool about nests that is allows you to use your preferred transport layer from the multiple ones it offers and you can easily change it in the configuration without changing the code later (say you are using Tcp and after a while, you wanted to switch over to a message queue like RMQ you just have to change it in the configuration of the module without changing anything in the code)

Before we jump into coding (I know it took too long but this is not your average code tutorial blog we need to understand what exactly each piece of code does when we write it) we need to discuss one last thing that is the types of microservices, in this context ,we are focusing on types regarding how are req-res are handled and we will focus on 2 types:

Request-response (Message pattern):

Here we send a message( which is just a piece of data) over to the right service over a transport layer, and then wait for it to return a response to use

Event-based

Here we are also sending a message or in this case, it is called an event but we don't actually expect or wait for a response instead side effects are just happening in the appropriate service(s) In most cases events are unidirectional meaning that the data only goes in one way

Event vs Req-Res

now that we have a basic understanding of what micro services architecture is all about let's dive into how to implement it using nestJs

Example of A micro Services System

here is what a micro Services system look like

Image description

NestJs Implementation:

We will try to build a simple e-commerce example to showcase microServices in nests

  1. First we Create a normal nests application this application is often called the producer because unlike other nodes it is an HTTP app that is responsible for getting the requests and sending them to the appropriate service

  2. then we need to install the microServices module

$ npm i --save @nestjs/microservices
Enter fullscreen mode Exit fullscreen mode

3 . Install the transport layer here I'm using Rabbit mq feel free to use anyone you like (check the documentation for supported Transport layers)

$ npm i --save amqplib amqp-connection-manager
Enter fullscreen mode Exit fullscreen mode

We define a simple dto for validation

  import { IsEmail, IsNumber, IsString } from 'class-validator';

export class orderDto {
  @IsString()
  product: string;
  @IsEmail()
  email: string;
  @IsNumber()
  quantity: number;
}
Enter fullscreen mode Exit fullscreen mode

Then we register our services inside the app Module or the feature module if you want better separation, using the microservices module that we just downloaded here we will only use the payment and orders service

 import { OrdersController } from './orders.controller';
import { ClientsModule, Transport } from '@nestjs/microservices';

 @Module({
  imports: [
    ClientsModule.register([
      {
        name: 'ORDER_SERVICE',
        transport: Transport.RMQ,
        options: {
          URLs: ['amqp://localhost:5672'],
          queue: 'order_queue',
          queueOptions: {
            durable: true, // Make the queue persistent
          },
          // Specify an exchange
        },
      },
    ]),
  ClientsModule.register([
      {
        name: 'PAYMENT_SERVICE',
        transport: Transport.RMQ,
        options: {
          URLs: ['amqp://localhost:5672'],
          queue: 'payment_queue',
          queueOptions: {
            durable: true, // Make the queue persistent
          },
          // Specify an exchange
        },
      },
    ]),

  ],
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class OrdersModule {}

Enter fullscreen mode Exit fullscreen mode

Here I'm using RabbitMq which is a powerful message queue
a message queue: is a transport layer that uses the FIFO Data structure and holds the messages in a queue until the first message gets processed by the consumer (service), what is cool about it is that if the consumer is down it will wait until it is up again (unlike tcp for example which if the service is down the message will just be lost)

Here is a simple controller that handles 2 requests

/* eslint-disable prettier/prettier */
import { Body, Controller, Post } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { orderDto } from './orderDto';

@Controller('orders')
export class OrdersController {
  constructor(private read-only ordersService: OrdersService) {
  }
  @Post()
  OrderAdded(@Body() order:orderDto){
     return this.ordersService.handleOrder(order);
  }
  @Post('/pay')
  payOrder(@Body() order:orderDto){
    return this.ordersService.handle payment(order);
  }
}
Enter fullscreen mode Exit fullscreen mode

And this is the service

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

@Injectable()
export class OrdersService {
   constructor(@Inject("ORDER_SERVICE") private RSERVICE:ClientProxy,@Inject('PAYMENT_SERVICE') private PAYMENTQueue:ClientProxy  ){}
  handleOrder(order :orderDto){
   this.RSERVICE.emit('ORDER_PLACED',order);
    return {message:"ORDER ADDED!"}
  }
   handle payment(order: orderDto) {
    this.PAYMENTQueue.emit('PAYMENT',order);
    return {
      message: 'PAYMENT DONE!',
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here is where the magic happens first we Inject services registered earlier in the module and then we emitting events the first String in the emit method is the event pattern which is how the service now what logic to execute and the second param is the payload which is the actual data that the service needs to work with.
here we are using .emit because we are using event-based architecture
if you want to use a message you just use .send instead

And then we need to create another nest application which is the service itself here is how to define the main of the app

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Transport } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.createMicroservice(AppModule, {
    transport: Transport.RMQ,
    options: {
      URLs: ['amqp://localhost:5672'],
      queue: 'order_queue',
      queueOptions: {
        durable: true,
      },
    },
  });
  await app.listen();
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

what changes that we create a microService instead of a nest application and then we define the appropriate transport layer that this service uses to communicate with the producer
and here is to define the controller to handle upcoming events

 import { Controller } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { Ctx, EventPattern, Payload, RmqContext } from '@nestjs/microservices';
import { OrderDto } from './orderDto';

@Controller('orders')
export class OrdersController {
  constructor(private readonly ordersService: OrdersService) {}
  @EventPattern('ORDER_PLACED')
  handleOrders(@Payload() order: OrderDto, @Ctx() context: RmqContext) {
    return this.ordersService.handleOrder(order);
  }
}
Enter fullscreen mode Exit fullscreen mode

note that the Event Pattern is the same one we sent Earlier
here we get Acces to the payload that we send and the Excution context which contains some metadata that are out of the scope of this article
what cool now is that we can register another Service inside this service and send events to it as well like we're doing here in the service

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

@Injectable()
export class OrdersService {
  constructor(
    @Inject('PAYMENT_SERVICE') private readonly paymentClient: ClientProxy,
  ) {}
  handleOrder(order: OrderDto) {
    console.log('PROCESSING ORDER', order);
    console.log('SENDING ORDER TO PAYMENT SERVICE');
    this.paymentClient.emit('PAYMENT', order);
  }
}
Enter fullscreen mode Exit fullscreen mode

and Payment Client is another service built just like this one

In this article, we explored how to build a microservices architecture using NestJS and RabbitMQ. We discussed the benefits of microservices, such as scalability, fault tolerance, and separation of concerns, and demonstrated how NestJS simplifies inter-service communication.

By leveraging RabbitMQ as a message broker, we ensured reliable, asynchronous communication between services, making our system more resilient and efficient. The flexibility of NestJS also allows us to switch transport layers easily, giving us the freedom to adapt to different use cases.

As you continue building microservices, consider factors like service discovery, monitoring, and security to enhance your system further. Experiment with different transport layers, and explore tools like Kubernetes for orchestration. Microservices can be complex, but with the right tools and architecture, they can unlock powerful scalability and maintainability for your applications. 🚀

Top comments (0)