Microservices with NestJS
Introduction to microservices architecture and how to build microservices using NestJS with gRPC or message queues (e.g., RabbitMQ, Kafka).
Inter-service Communication in NestJS
Understanding Inter-service Communication Patterns
In a microservices architecture, services need to communicate with each other to fulfill user requests. Choosing the right inter-service communication pattern is crucial for performance, reliability, and scalability. These patterns dictate how services interact, exchange data, and handle errors.
There are two main categories: synchronous and asynchronous communication.
Synchronous Communication
Synchronous communication involves direct request-response interactions between services. The calling service waits for a response from the called service before continuing its operations. This is often used when a service needs immediate feedback or relies on the result of another service to proceed.
gRPC
gRPC is a high-performance, open-source RPC (Remote Procedure Call) framework developed by Google. It uses Protocol Buffers for message serialization, providing efficient data transfer and strong type checking. It is a good choice for internal service communication where performance and efficiency are critical.
Here's a conceptual example of gRPC in NestJS. Note: This is just a snippet and would require setting up protobuf definitions and gRPC services.
// product.proto (Example)
syntax = "proto3";
package product;
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product) {}
}
message GetProductRequest {
string id = 1;
}
message Product {
string id = 1;
string name = 2;
float price = 3;
}
// product.controller.ts (NestJS)
import { Controller, Inject } from '@nestjs/common';
import { ClientGrpc, GrpcMethod } from '@nestjs/microservices';
import { ProductService } from './interfaces/product-service.interface';
interface GetProductRequest {
id: string;
}
@Controller('product')
export class ProductController {
@Inject('PRODUCT_PACKAGE')
private readonly client: ClientGrpc;
private productService: ProductService;
onModuleInit() {
this.productService = this.client.getService('ProductService');
}
@GrpcMethod('ProductService', 'GetProduct')
getProduct(data: GetProductRequest) {
return this.productService.getProduct(data);
}
}
REST
REST (Representational State Transfer) is a widely used architectural style that leverages HTTP methods (GET, POST, PUT, DELETE) to interact with resources. It is commonly used for exposing APIs and can be suitable for both internal and external service communication. REST is generally easier to implement and debug than gRPC, particularly when the services are not all under your direct control.
Example REST Controller in NestJS:
// product.controller.ts
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
@Controller('products')
export class ProductController {
private products = [
{ id: '1', name: 'Product A', price: 20.00 },
{ id: '2', name: 'Product B', price: 30.00 },
];
@Get(':id')
getProduct(@Param('id') id: string) {
const product = this.products.find(p => p.id === id);
if (!product) {
throw new NotFoundException(`Product with ID ${id} not found`);
}
return product;
}
}
Asynchronous Communication
Asynchronous communication allows services to interact without requiring an immediate response. Services send messages to a message broker or queue, and other services can subscribe to these messages to process them. This decouples the services, improving resilience and scalability. It's well-suited for handling background tasks, event-driven architectures, and situations where immediate confirmation is not necessary.
Message Queues (e.g., RabbitMQ, Kafka)
Message queues act as intermediaries between services. One service publishes a message to the queue, and another service (or multiple services) subscribes to that queue to consume the message. This enables loose coupling and allows services to scale independently.
Example with RabbitMQ and NestJS:
// app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
@Module({
imports: [
ClientsModule.register([
{
name: 'PRODUCT_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://guest:guest@localhost:5672'],
queue: 'products_queue',
queueOptions: {
durable: false
},
},
},
]),
],
controllers: [AppController],
})
export class AppModule {}
// app.controller.ts (Publisher)
import { Controller, Inject, Post, Body } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
interface CreateProductEvent {
name: string;
price: number;
}
@Controller()
export class AppController {
@Inject('PRODUCT_SERVICE') private readonly client: ClientProxy;
@Post('products')
createProduct(@Body() product: CreateProductEvent) {
this.client.emit('product_created', product); // Emit the event
return { message: 'Product creation event sent' };
}
}
// product.controller.ts (Subscriber in another microservice)
import { Controller, EventPattern } from '@nestjs/common';
interface CreateProductEvent {
name: string;
price: number;
}
@Controller()
export class ProductController {
@EventPattern('product_created') // Listen for the event
handleProductCreated(data: CreateProductEvent) {
console.log('Product Created Event Received:', data);
// Process the event (e.g., create a product in the database)
}
}
Request/Response and Publish/Subscribe Patterns
Request/Response
The Request/Response pattern is the foundation of synchronous communication. One service sends a request to another service and waits for a response. REST and gRPC are prime examples of this pattern. The requesting service is blocked until it receives a response or a timeout occurs.
Publish/Subscribe
The Publish/Subscribe (Pub/Sub) pattern is a core asynchronous communication pattern. One service (the publisher) sends messages to a topic or exchange, and multiple services (the subscribers) can subscribe to that topic or exchange to receive the messages. This decouples the publisher from the subscribers, allowing them to evolve independently. Message queues (RabbitMQ, Kafka) are commonly used to implement the Pub/Sub pattern. NestJS's `@EventPattern` decorator facilitates this pattern when used with message queue transports.