NestJSでgRPCを使ってみた
はじめに
NestJS が気になって調査中です。
NestJS で gRPC 使うとどうなるのかなと思い試してみました。
下記サイトを参考にさせていただきました。
基本的には、1-2 のブログをマネさせてもらって、NestJS のバージョンアップで動かなかった部分や、proto から protoc を使ってコード自動生成する部分を追加修正してます。
- 【NestJS】NestJS で gRPC を使ってみた - server 編 - 開発覚書はてな版
- 【NestJS】NestJS で gRPC を使ってみた - client 編 - 開発覚書はてな版
- nest/sample/04-grpc at master · nestjs/nest
- gRPC - Microservices | NestJS - A progressive Node.js framework
- NestJS で gRPC API 作るならコード生成は ts-proto で決まり - Qiita
環境準備
NestJS
下記コマンドで、NestJS をインストールして、環境を構築します。
npm i -g @nestjs/cli
nest new project-name
Documentation | NestJS - A progressive Node.js framework
proto ファイルからのコード生成
下記コマンドで、Protocol Buffer Compiler
であるprotobuf
と TypeScript の型ファイルを作るts-proto
をインストールします。
brew install protobuf
npm install ts-proto
Protocol Buffer Compiler Installation | gRPC
stephenh/ts-proto: An idiomatic protobuf generator for TypeScript
コード
proto と型コード生成
スキーマ駆動開発ということで、まずは、proto。
proto はこんな感じです。rpc はパスカルケース(Language Guide)になってます。
syntax = "proto3";
package sample;
service AppService {
rpc FindOne (SampleDataById) returns (SampleData) {}
}
message SampleDataById {
int32 id = 1;
}
message SampleData {
int32 id = 1;
string name = 2;
}
下記コマンドで、型が書かれたコードを生成します。
protoc --ts_proto_opt=nestJs=true --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=.. ./src/proto/sample.proto
ts-proto/NESTJS.markdown at main · stephenh/ts-proto
生成されたコードは下記の通り。interface 等が定義されてます。
このコードはサーバー側でもクライアント側でも使用するので、両方のフォルダにコピーしておきます。
/* eslint-disable */
import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";
import { Observable } from "rxjs";
export const protobufPackage = "sample";
export interface SampleDataById {
id: number;
}
export interface SampleData {
id: number;
name: string;
}
export const SAMPLE_PACKAGE_NAME = "sample";
export interface AppServiceClient {
findOne(request: SampleDataById): Observable<SampleData>;
}
export interface AppServiceController {
findOne(
request: SampleDataById
): Promise<SampleData> | Observable<SampleData> | SampleData;
}
export function AppServiceControllerMethods() {
return function (constructor: Function) {
const grpcMethods: string[] = ["findOne"];
for (const method of grpcMethods) {
const descriptor: any = Reflect.getOwnPropertyDescriptor(
constructor.prototype,
method
);
GrpcMethod("AppService", method)(
constructor.prototype[method],
method,
descriptor
);
}
const grpcStreamMethods: string[] = [];
for (const method of grpcStreamMethods) {
const descriptor: any = Reflect.getOwnPropertyDescriptor(
constructor.prototype,
method
);
GrpcStreamMethod("AppService", method)(
constructor.prototype[method],
method,
descriptor
);
}
};
}
export const APP_SERVICE_NAME = "AppService";
サーバー
まず、下記コマンドで、依存してるパッケージをインストールしておきます。
npm install @grpc/grpc-js
npm install @nestjs/microservices
メインでは、INestMicroservice
のインターフェースを持つapp
を作って、listen()
します。
import { NestFactory } from '@nestjs/core';
import { Transport, GrpcOptions } from '@nestjs/microservices';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<GrpcOptions>(AppModule, {
transport: Transport.GRPC,
options: {
url: 'localhost:5000',
package: 'sample',
protoPath: join(__dirname, 'proto/sample.proto'),
},
});
await app.listen();
}
bootstrap();
参考サイトに書かれていたlistenAsync()
は deprecated していて、それっぽいものとしてlisten()
を使ったら期待通り動きました。
Migration guide - FAQ | NestJS - A progressive Node.js framework
コントローラーはこんな感じです。AppServiceController
等は proto から生成したインターフェースを使ってます。
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { AppServiceController, SampleData, SampleDataById } from './sample';
@Controller()
export class AppController implements AppServiceController {
@GrpcMethod('AppService')
findOne(data: SampleDataById): SampleData {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
] as SampleData[];
const filteredItems = items.filter((item) => item.id === data.id);
return filteredItems.length > 0 ? filteredItems[0] : ({} as SampleData);
}
}
module はこんな感じ。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}
NestJS の設定はこんな感じです。
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
"proto/**/*"
]
}
}
実行はこんな感じです。
npm start
CLI クライアントで動作確認
ktr0731/evansを使って動作確認してみました。下記コマンドで期待通り動いていることを確認できます。
echo '{"id": 2}' | evans --proto server/src/proto/sample.proto -p 5000 cli call sample.AppService.FindOne
Mac の場合インストールは brew で実行できます。
brew install evans
ktr0731/evans: Evans: more expressive universal gRPC client
クライアント
下記コマンドで依存してるパッケージをインストールします。サーバー側と一緒です。
npm install @grpc/grpc-js
npm install @nestjs/microservices
メインは、BFF (Backend For Frontend)を想定して port 3000 で API サーバーを立ち上げます。
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
サービスでは、proto から生成したAppServiceClient
を取得してfindOne()
を呼び出しています。
import { Injectable, OnModuleInit, Inject } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import { AppServiceClient, SampleData, SampleDataById } from './sample';
@Injectable()
export class AppService implements OnModuleInit {
private sampleService: AppServiceClient;
constructor(@Inject('SAMPLE_PACKAGE') private client: ClientGrpc) {}
onModuleInit() {
this.sampleService = this.client.getService<AppServiceClient>('AppService');
}
getSampleData(): Observable<SampleData> {
return this.sampleService.findOne({ id: 1 } as SampleDataById);
}
}
コントローラーでは、上記サービスにルーティングしてます。
import { Controller, Get } from '@nestjs/common';
import { Observable } from 'rxjs';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
call(): Observable<any> {
return this.appService.getSampleData();
}
}
モジュールでは、依存関係や gRPC の設定を記載しています。
import { Module } from '@nestjs/common';
import { Transport, ClientsModule } from '@nestjs/microservices';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
ClientsModule.register([
{
name: 'SAMPLE_PACKAGE',
transport: Transport.GRPC,
options: {
url: 'localhost:5000',
package: 'sample',
protoPath: join(__dirname, 'proto/sample.proto'),
},
},
]),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
NestJS の設定はこんな感じです。
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
"proto/**/*"
]
}
}
実行はこんな感じです。
npm start
最後に、サーバーとクライアントのプログラムを実行することで、ブラウザで、http://localhost:3000にアクセスすることで、期待通り json が返ってきていることを確認できます。
おわりに
以前、メルカリのブログを参考に TypeScript で gRPC を使ってみましたが、今回 NestJS 上で違うライブラリを使って動作確認しました。gRPC のツールが複数あって、何が違うのか、まだいまいちわかってませんが、NestJS 使った場合は今回のやり方がシンプルで良さそうな気がしました。今後、Meta データをやりとりする方法など、もうちょい調べてみようと思います。
OK Google, Protocol Buffers から生成したコードを使って Node.js で gRPC 通信して | メルカリエンジニアリング
gRPC x TypeScript を Docker で動かしてみた
gRPC x TypeScript で メタデータの送受信
Discussion