Step-by-Step Guide: gRPC Microservices in NestJS

Gaurav Agrawal
Level Up Coding
Published in
5 min readAug 6, 2023

--

source: https://unsplash.com/photos/ahD461U6nwg

In this step-by-step tutorial, we will learn how to use the gRPC protocol to enable communication between multiple microservices written in Nest.js. With the gRPC protocol, you can facilitate efficient and seamless inter-service communication, enhancing your Nest.js application’s performance and scalability.

Let’s delve into what we will be building for this tutorial.

We will be creating two micro-services, namely order-service and user-service.

Within the Order Service, we will implement an API called myOrders which will return dummy orders of a user. However, before returning the orders, the service will communicate with the user-service using gRPC (Google Remote Procedure Call) to ensure that the user is a valid and active user in the system.

This is a simulated use case designed to illustrate micro-services communicating with each other. Keep in mind that your real-world use case might differ, such as the order service obtaining real-time user details to send notifications via mobile or email.

Setting up the Project

In the setup part, we will be covering GRPC specific items mostly, assuming you have basic idea of Nest Js.

Initialise two Nest-js projects user-service and order service using the nest js cli command

nest new order_service
nest new user_service

Install GRPC specific dependencies:

In GRPC we define the API’s in protocol buffer or proto files, and our Nest Js clients needs to be understand these interface, for this we will be using ts-proto package, which will automatically generate the adapter nest js code for our proto definitions.

npm i --save @grpc/grpc-js @grpc/proto-loader
npm i protoc-gen-ts_proto
npm install ts-proto

The project structure would look something like this

user-service project structure

Here, we will have our user module in src/userand protodirectory to keep our GRPC service definitions.

In proto/user.proto we define a basic getUser by Id API.

syntax = "proto3";

package user;

message GetUserRequest {
string id = 1;
}

message User {
string id = 1;
string name = 2;
bool isActive = 3;
}

service UserService {
rpc getUser(GetUserRequest) returns (User) {}
}

And then we run the following command to generate it’s Nest-JS supported typescript client.

protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=nestJs=true,importSuffix=.js:. ./src/proto/user/user.proto

Note: Make sure you have protoc installed in your system for the following command to be able to work.

This will generate the User Client for us to use by our Micro-service, in the same folder → proto/user.ts

/* eslint-disable */
import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";
import { Observable } from "rxjs";

export const protobufPackage = "user";

export interface GetUserRequest {
id: string;
}

export interface User {
id: string;
name: string;
isActive: boolean;
}

export const USER_PACKAGE_NAME = "user";

export interface UserServiceClient {
getUser(request: GetUserRequest): Observable<User>;
}

export interface UserServiceController {
getUser(request: GetUserRequest): Promise<User> | Observable<User> | User;
}

export function UserServiceControllerMethods() {
return function (constructor: Function) {
const grpcMethods: string[] = ["getUser"];
for (const method of grpcMethods) {
const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
GrpcMethod("UserService", method)(constructor.prototype[method], method, descriptor);
}
const grpcStreamMethods: string[] = [];
for (const method of grpcStreamMethods) {
const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
GrpcStreamMethod("UserService", method)(constructor.prototype[method], method, descriptor);
}
};
}

export const USER_SERVICE_NAME = "UserService";

The same proto folder and type script client needs to be in Order-Service as well and they need to be in sync, for now we can just copy paste the src/proto folder from user-service to order service.

In the user service controller, we can expose our GRPC apis

import { Controller, Logger } from '@nestjs/common';
import { GetUserRequest, User, UserServiceController, UserServiceControllerMethods } from '../proto/user/user';

@Controller()
@UserServiceControllerMethods()
export class UserController implements UserServiceController {
private readonly logger = new Logger(UserController.name);
getUser(request: GetUserRequest): Promise<User> {
this.logger.log(request);
// Implement your logic to retrieve the item based on the request
// You can use the request.itemId to fetch the specific item from your data source
const item: User = {
id: request.id,
name: 'Sample Item',
isActive: true
};
return Promise.resolve(item);
}
}

In the user module, we would be registering our grpc client module.

import { Module } from "@nestjs/common";
import { ClientsModule, Transport } from "@nestjs/microservices";
import { UserController } from "./user.controller";

@Module({
imports: [
ClientsModule.register([
{
name: 'USER_PACKAGE',
transport: Transport.GRPC,
options: {
package: 'user',
protoPath: 'src/proto/user/user.proto',
},
},
])
],
controllers: [UserController],
providers: [],
})
export class UserModule {}

So, that’s it, now in the main.ts for our app, we just need to enable our grpc micro-service.

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.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC, // Use Transport.GRPC for gRPC
options: {
url: 'localhost:5000',
protoPath: join(__dirname, '../src/proto/user/user.proto'),
package: 'user'
}});
await app.startAllMicroservices();
await app.listen(3000);
}
bootstrap();

And, our getUser api is ready to be consumed by our order-service client.

Coming back to Order-service, we would need to consume the getUser api hence establishing this micro-service communication, for which we need to setup the grpc client and use the apis available with it.

order-service project structure

In the order module we first need to register the client in the following way:

import { Module } from "@nestjs/common";
import { ClientsModule, Transport } from "@nestjs/microservices";
import { OrderController } from "./order.controller";
import { OrderService } from "./order.service";

@Module({
imports: [
ClientsModule.register([
{
name: 'USER_PACKAGE',
transport: Transport.GRPC,
options: {
url: 'localhost:5000',
package: 'user',
protoPath: 'src/proto/user/user.proto',
},
},
])
],
controllers: [OrderController],
providers: [OrderService],
exports: []
})
export class OrderModule {}

And, then in the order service we can use the grpc api to get the user details for the myOrder api

import { BadRequestException, Inject, Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { ClientGrpc } from "@nestjs/microservices";
import { UserServiceClient } from "../proto/user/user";
import { firstValueFrom } from "rxjs";


@Injectable()
export class OrderService implements OnModuleInit {

private logger = new Logger(OrderService.name);

private userServiceClient: UserServiceClient;

constructor(
@Inject('USER_PACKAGE')
private grpcClient: ClientGrpc,
) {}

onModuleInit() {
this.userServiceClient = this.grpcClient.getService<UserServiceClient>('UserService');
}

async getOrders(userId: string) {
const user = await firstValueFrom(this.userServiceClient.getUser({ id: userId }));
this.logger.log(user);
if(!user || !user.isActive) throw new BadRequestException("User Not found or inactive");
return {
'id': 1,
'name': 'Dummy Order',
'status': 'PAID',
'price': '10$'
}
}
}

and finally in the order controller we can expose our apis.

import { Controller, Get, Inject, OnModuleInit } from '@nestjs/common';
import { UserServiceClient } from '../proto/user/user';
import { ClientGrpc } from '@nestjs/microservices';
import { OrderService } from './order.service';


@Controller('order')
export class OrderController {

constructor(
private orderService: OrderService,
) {}


@Get('myOrders')
getOrders() {
return this.orderService.getOrders('userId1'); //Dummy user id
}
}

So, in this way, we can build a basic setup for two different micro-services to communicate with each other using GRPC.

You can find the complete working repo on my Github @gaurav1999

--

--