Microservices with NestJS
Introduction to microservices architecture and how to build microservices using NestJS with gRPC or message queues (e.g., RabbitMQ, Kafka).
gRPC with NestJS
Introduction to gRPC
gRPC (gRPC Remote Procedure Calls) is a modern, high-performance remote procedure call (RPC) framework that can run in any environment. It efficiently connects services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication.
Understanding gRPC and Protocol Buffers
gRPC uses Protocol Buffers (protobuf) as its Interface Definition Language (IDL) and underlying message interchange format. Here's a breakdown:
- gRPC: A framework for building distributed systems and applications. It's based on the concept of defining services, methods, and message types, and then automatically generating client and server code.
- Protocol Buffers (Protobuf): A language-neutral, platform-neutral extensible mechanism for serializing structured data. Think of it as a more efficient and robust alternative to JSON or XML.
The combination of gRPC and Protobuf provides several advantages:
- Efficiency: Protobuf is significantly more compact and faster to serialize/deserialize than JSON or XML, leading to reduced network bandwidth and improved performance.
- Strong Typing: Protobuf enforces a schema, ensuring data consistency and reducing the risk of errors.
- Code Generation: gRPC automatically generates client and server stubs from the Protobuf definition, simplifying development and reducing boilerplate code.
Defining gRPC Services and Messages
The first step in using gRPC is to define the service and message types in a .proto
file. This file acts as the contract between the client and server.
Here's an example of a simple .proto
file:
syntax = "proto3";
package hero;
service HeroesService {
rpc FindOne (HeroById) returns (Hero);
}
message HeroById {
int32 id = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
Explanation:
syntax = "proto3";
: Specifies the Protobuf version.package hero;
: Defines a package to avoid naming conflicts.service HeroesService { ... }
: Defines the gRPC service with a method calledFindOne
.rpc FindOne (HeroById) returns (Hero);
: Declares a remote procedure call (RPC) namedFindOne
. It takes aHeroById
message as input and returns aHero
message.message HeroById { ... }
: Defines the structure of theHeroById
message, containing an integerid
.message Hero { ... }
: Defines the structure of theHero
message, containing an integerid
and a stringname
.
Implementing gRPC Servers and Clients using NestJS
NestJS provides excellent support for gRPC through its modules, decorators, and libraries. Here's a general outline of how to implement a gRPC server and client:
Server Implementation
- Install Dependencies:
npm install @nestjs/microservices @grpc/grpc-js @nestjs/platform-express reflect-metadata rxjs class-transformer class-validator protobufjs
- Generate gRPC Stubs: Use the
grpc_tools_node_protoc
andts-proto
to generate TypeScript interfaces and classes from your.proto
file. This usually involves a command line command such as:protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/hero --ts_proto_opt=nestJs=true,fileSuffix=-grpc.pb --proto_path=./proto hero.proto
This creates a file usually named like
hero-grpc.pb.ts
that contains the generated code. - Configure the Microservice: In your NestJS `main.ts` (or other bootstrapping file), configure the microservice using the `ClientsModule` or `Transport.GRPC`.
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { join } from 'path'; async function bootstrap() { const app = await NestFactory.createMicroservice
(AppModule, { transport: Transport.GRPC, options: { package: 'hero', protoPath: join(__dirname, './hero/hero.proto'), url: 'localhost:50051', // Or your chosen port }, }); await app.listen(); } bootstrap(); - Implement the gRPC Service: Create a NestJS service that implements the methods defined in your Protobuf service definition. Use the `@GrpcMethod()` decorator to map the service methods.
import { Controller, OnModuleInit } from '@nestjs/common'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { HeroById, Hero } from './hero/hero-grpc.pb'; import { Metadata } from '@grpc/grpc-js'; import { Observable, of, throwError } from 'rxjs'; interface HeroService { findOne(data: HeroById, metadata?: Metadata): Observable
; } @Controller() export class HeroController implements HeroService { @GrpcMethod('HeroesService', 'FindOne') // service name and method name findOne(data: HeroById, metadata?: Metadata): Observable { const items = [ { id: 1, name: 'John' }, { id: 2, name: 'Doe' }, ]; const hero = items.find(({ id }) => id === data.id); if(!hero){ return throwError(()=> new RpcException({ code: 5, message: "Hero not found!"})); } return of(hero); } } - Start the Server: Run your NestJS application.
Client Implementation
- Install Dependencies: (Same as server dependencies)
- Generate gRPC Stubs: (Same as server)
- Import the gRPC Service: Import the generated gRPC service client.
- Use the Client: Inject the gRPC service client into your NestJS component or service using the `@Client()` decorator and `ClientGrpcProxy` class. Use the client to make gRPC calls.
import { Injectable, OnModuleInit, Inject } from '@nestjs/common'; import { ClientGrpc, Client } from '@nestjs/microservices'; import { HeroById, Hero } from './hero/hero-grpc.pb'; import { Observable } from 'rxjs'; import { ClientGrpcProxy } from '@nestjs/microservices'; interface HeroService { findOne(data: HeroById): Observable
; } @Injectable() export class AppService implements OnModuleInit { @Client({ transport: Transport.GRPC, options: { package: 'hero', protoPath: join(__dirname, './hero/hero.proto'), }, }) client: ClientGrpc; private heroService: HeroService; onModuleInit() { this.heroService = this.client.getService ('HeroesService'); } getHero(id:number): Observable { return this.heroService.findOne({id: id}); } } - Call the gRPC Service From your application code:
constructor(private readonly appService: AppService) {} @Get('hero/:id') getHero(@Param('id', ParseIntPipe) id: number): Observable
{ return this.appService.getHero(id); }
This provides a basic overview. You'll need to adapt the code based on your specific service definition and application requirements.