Микросервисы gRPC в NestJS: пошаговое руководство

В процессе иллюстрации возможностей gRPC создадим два микросервиса: службу заказа (order-service) и службу пользователя (user-service).

В службу заказов (order-service) внедрим API MyOrders, возвращающий фиктивные заказы пользователя. Прежде чем вернуть заказ, служба связывается с помощью gRPC с user-service для проверки прав пользователя в системе.

Такой смоделированный вариант позволит демонстрировать взаимодействие между микросервисами. При этом возможны индивидуальные отличия, например служба заказа может получать данные пользователя в режиме реального времени для отправки уведомлений на мобильный телефон или по электронной почте.

Настройка проекта

Предполагается, что вы знакомы с основами NestJS. Сначала определим специфичные для gRPC элементы.

С помощью командной строки NestJS инициализируем проекты user-service и order-service.

nest new order_service
nest new user_service

Установим специфичные для gRPC зависимости.

В gRPC определяем API в файлах буфера протокола или proto. Наши клиенты NestJS должны понимать этот интерфейс, и для этого мы будем использовать пакет ts-proto, который автоматически сгенерирует код адаптера NestJS для определений proto.

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

Примерная структура проекта:

Структура проекта user-service

Здесь будет пользовательский модуль (user) в src/user и каталог proto для хранения определений сервисов gRPC.

В proto/user.proto определим базовый getUser с помощью API Id.

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) {}
}

Для создания клиента TypeScript, поддерживаемого Nest JS, запускаем следующую команду.


protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_p

Примечание: убедитесь, что в вашей системе установлен компилятор protoc, чтобы могла выполняться следующая команда.

В результате в той же папке proto/user.ts будет создан пользовательский клиент (User Client) для использования микросервисом.

/* 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";

Папка proto и клиент TypeScript должны быть синхронизированными в службе заказа (order-service). После этого можно просто скопировать/вставить папку src/proto из службы пользователя (user-service) в службу заказа (order-service).

В контроллер user-service можно предоставлять API gRPC.

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);
// Реализуйте свою логику для получения элемента на основе запроса.
// Вы можете использовать request.itemId для получения определенного элемента из вашего источника данных.
const item: User = {
id: request.id,
name: 'Sample Item',
isActive: true
};
return Promise.resolve(item);
}
}

В пользовательском модуле зарегистрируем клиентский модуль gRPC.

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 {}

Теперь в файле приложения main.ts нужно просто разрешить микросервис 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.create(AppModule);
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.GRPC, // Используем Transport.GRPC для gRPC
options: {
url: 'localhost:5000',
protoPath: join(__dirname, '../src/proto/user/user.proto'),
package: 'user'
}});
await app.startAllMicroservices();
await app.listen(3000);
}
bootstrap();

Теперь API-интерфейс getUser готов к использованию клиентом order-service.

В сервисе заказов нужно будет использовать API getUser, реализуя это микросервисное взаимодействие, для чего нужно настроить клиент gRPC и использовать доступные с ним API.

Структура проекта order-service

В модуле заказа сначала нужно зарегистрировать клиента следующим образом:

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 {}

Затем в службе заказов мы cможем использовать API gRPC, чтобы получить сведения о пользователе для API myOrder.

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$'
}
}
}

И наконец, мы сможем предоставить API в контроллере заказов.

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
}
}

Таким образом можно создать базовую настройку для взаимодействия с помощью gRPC между двумя микросервисами.

Полный рабочий репозиторий находится на Github.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Gaurav Agrawal: Step-by-Step Guide: gRPC Microservices in NestJS

Предыдущая статьяСобытийно-ориентированная архитектура
Следующая статьяКак повысить эффективность кода Python с помощью кэширования