技術っぽいことを書いてみるブログ

PythonとかVue.jsとか技術的なことについて書いていきます。

NestJsでgRPCをts-protoを使ってやってみた(Server編)

背景

NestJsでgRPCのサーバー編をやってみましたが、
protoファイルで定義したmessageと同様にinterfaceを作成したり、結構煩雑なのでproto-tsを導入し、ある程度の自動化を図ってみます

準備

protocのインストール

インストールというか、protocをダウンロードしてPATHを通すだけです。
ダウンロードは、以下から実施しましょう。

Releases · protocolbuffers/protobuf · GitHub

proto-tsのインストール

こちらはプロジェクトフォルダでnpmインストールします。

npm i ts-proto

.protoの定義

proto/sample.proto

syntax = "proto3";

import "google/protobuf/empty.proto";

package sample;

service SampleService {
    rpc FindOne (SampleRequest) returns (SampleResponse) {}
    rpc getAll (google.protobuf.Empty) returns (Sample2Response) {}
}

message SampleRequest {
    int32 id = 1;
}

message SampleResponse {
    int32 id = 1;
    string word = 2;
}
message Sample2Response {
    repeated SampleResponse sampleResponses = 1;
}
  • メソッドを二つ定義しました。

protocコマンドを--pluginオプションで、proto-tsを指定して実行する

protoc --plugin=protoc-gen-ts_proto=".\\node_modules\\.bin\\protoc-gen-ts_proto.cmd" --ts_proto_opt=nestJs=true --ts_proto_opt=outputClientImple=false --ts_proto_opt=addGrpcMetadata=true --ts_proto_out=src -Isrc\proto .\src\proto\sample.proto
  • protocの--pluginでproto-tsのcmdファイルを指定する。
    • proto-tsをインストールすると、node_modules.bin配下に作成されているはず。
    • Windows環境の場合は、--plugin=protoc-gen-ts_proto=".\\node_modules\\.bin\\protoc-gen-ts_proto.cmd"のように書かないといけない
      • 公式のQuickStartにも書いてあった・・・。
  • -ts_proto_opt=nestJs=trueとすることで、nestjsに特化したtsファイルを出力してくれる。
    • このオプションを指定しないと全然違うものが出来上がります。

コマンド実行で生成されるtsファイル

/* eslint-disable */
import { Metadata } from "@grpc/grpc-js";
import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";
import { Observable } from "rxjs";
import { Empty } from "./google/protobuf/empty";

export const protobufPackage = "sample";

export interface SampleRequest {
  id: number;
}

export interface SampleResponse {
  id: number;
  word: string;
}

export interface Sample2Response {
  sampleResponses: SampleResponse[];
}

export const SAMPLE_PACKAGE_NAME = "sample";

export interface SampleServiceClient {
  findOne(request: SampleRequest, metadata?: Metadata): Observable<SampleResponse>;

  getAll(request: Empty, metadata?: Metadata): Observable<Sample2Response>;
}

export interface SampleServiceController {
  findOne(
    request: SampleRequest,
    metadata?: Metadata,
  ): Promise<SampleResponse> | Observable<SampleResponse> | SampleResponse;

  getAll(request: Empty, metadata?: Metadata): Promise<Sample2Response> | Observable<Sample2Response> | Sample2Response;
}

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

export const SAMPLE_SERVICE_NAME = "SampleService";

controllerの定義

.protoから生成されたtsファイルを使用して、controllerを作成する

sample.controller.ts

import { Metadata } from '@grpc/grpc-js';
import {
  Sample2Response,
  SampleRequest,
  SampleResponse,
  SampleServiceController,
  SampleServiceControllerMethods,
} from './sample';
import { Empty } from './google/protobuf/empty';
import { Controller } from '@nestjs/common';

@Controller('sampele')
@SampleServiceControllerMethods()
export class SampleController implements SampleServiceController {
  private data: SampleResponse[] = [
    { id: 1111, word: 'hogehoge' },
    { id: 2222, word: 'fugafuga' },
  ];

  findOne(request: SampleRequest, metadata?: Metadata): SampleResponse {
    return this.data.find((a) => a.id === request.id);
  }

  getAll(request: Empty, metadata?: Metadata): Sample2Response {
    return { sampleResponses: this.data };
  }
}
  • コントローラークラスには、protoファイルから生成されたtsファイルにある@SampleServiceControllerMethods()を付与する。
  • また、コントローラークラスのinterfaceも生成されたtsファイルで定義してくれている(SampleServiceController)ので、実装する形で定義する。

main.tsへのprotoファイルのパス設定追加

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { join } from 'path';
import { GrpcOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<GrpcOptions>(AppModule, {
    transport: Transport.GRPC,
    options: {
      url: 'localhost:5000',
      package: ['postCode', 'sample'], //add 'sample'
      protoPath: [
        join(__dirname, 'proto/postCode.proto'),
        join(__dirname, 'proto/sample.proto'),  //add
      ],
    },
  });
  await app.listen();
}
bootstrap();

module.tsへの追加

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SampleController } from './sample.controller';

@Module({
  imports: [],
  controllers: [AppController, SampleController], // add SampleController
  providers: [AppService],
})
export class AppModule {}

実行してみる

今回は、BloomRPCで実行して確認します。

FindOne

getAll

想定どおりデータが取得できています!

今回のハマりどころ・・・

.protoファイルでパラメータなしを表すためgoogle.protobuf.Emptyを使用しましたが、
当初importをimport "google/protobuf/Empty.proto";を使用していました。(EmptyのEが大文字)
この状態で、nestjsの起動を行うと・・・

[Nest] 28756  - 2023/08/09 10:33:29   ERROR [Server] no such type: google.protobuf.Empty
Error: The invalid .proto definition (file at "undefined" not found)
    at ServerGrpc.loadProto (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:285:39)
    at ServerGrpc.bindEvents (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:42:34)
    at ServerGrpc.start (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:37:20)
    at ServerGrpc.listen (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:30:24)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)

C:\microservice-study\post-code-service\node_modules\protobufjs\src\namespace.js:382
        throw Error("no such type: " + path);
              ^
Error: no such type: google.protobuf.Empty
    at Service.lookupType (C:\microservice-study\post-code-service\node_modules\protobufjs\src\namespace.js:382:15)
    at Method.resolve (C:\microservice-study\post-code-service\node_modules\protobufjs\src\method.js:156:44)
    at Service.resolveAll (C:\microservice-study\post-code-service\node_modules\protobufjs\src\service.js:111:20)
    at Namespace.resolveAll (C:\microservice-study\post-code-service\node_modules\protobufjs\src\namespace.js:307:25)
    at Root.resolveAll (C:\microservice-study\post-code-service\node_modules\protobufjs\src\namespace.js:307:25)
    at Root.resolveAll (C:\microservice-study\post-code-service\node_modules\protobufjs\src\root.js:259:43)
    at loadProtosWithOptionsSync (C:\microservice-study\post-code-service\node_modules\@grpc\proto-loader\src\util.ts:80:14)
    at Object.loadSync (C:\microservice-study\post-code-service\node_modules\@grpc\proto-loader\src\index.ts:391:47)
    at ServerGrpc.loadProto (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:280:62)
    at ServerGrpc.bindEvents (C:\microservice-study\post-code-service\node_modules\@nestjs\microservices\server\server-grpc.js:42:34)

こんなエラーが出現しました。。。
これは、import "google/protobuf/empty.proto";(emptyのeが小文字)を使用することで、解消しました。

一応、こちらのIssueに書いていました・・・。
皆様、お気をつけて・・・

所感

Windows環境でのprotocコマンドの実行でいろいろエラーがでましたが、公式をちゃんと確認すれば解消しました。

いちおう、nestjsのサーバ編は一応できましたが、もうちょっと自動化を図りたいとことです。。。