尋常でないもふもふ

a software engineer blog

Nest.jsでgRPCを使ったマイクロサービス

Nest.js ではアプリケーションサーバとして動かすだけでなく、同時に gRPC として動かし、別のマイクロサービスと通信ができる設計になっている。

docs.nestjs.com

main.ts

(async () => {
  const app = await NestFactory.create(AppModule);

  const protoDir = join(__dirname, '..', 'protos');
  app.connectMicroservice({
    transport: Transport.GRPC,
    options: {
      url: '0.0.0.0:5000',
      package: 'rpc',
      protoPath: '/rpc/rpc.proto',
      loader: {
        keepCase: true,
        longs: Number,
        defaults: false,
        arrays: true,
        objects: true,
        includeDirs: [protoDir],
      },
    },
  });

  await app.startAllMicroservicesAsync();
  await app.listen(3000);
});

普通のアプリケーションサーバとして動かす必要がないなら await app.listen(3000);コメントアウトする。
マイクロサービスなので単一の .proto ファイルを読み込むレベルのシンプルなもので事足りると思うが、複数 proto を読み込みたい場合は includeDirsディレクトリを指定すればよい。ここは Nest.js 公式のサンプルになく、Issue あげたら教えてもらえた。この部分は @grpc/proto-loader が使われている。

rpc.controller.ts

Controller はデコレーターで gRPC のサービス名とメソッド名を指定する方式。
リクエスト/レスポンスの型情報は公式の grpc-tools を使うより protobuf.js で生成した方が JavaScript オブジェクトをそのまま型変換できて便利。

@Controller()
export class RpcController {
  constructor(private readonly championService: ChampionService, private readonly battleFieldService: BattleFieldService) {}

  @GrpcMethod('Rpc', 'GetChampion')
  async getChampion(req: GetChampionRequest): Promise<GetChampionResponse> {
    const obj = this.championService.getChampion(req.champion_id);
    return GetChampionResponse.create({champion: obj});
  }

  @GrpcMethod('Rpc', 'ListChampions')
  async listChampions(req: IEmpty): Promise<ListChampionsResponse> {
    const champions = this.championService.listChampions();
    return ListChampionsResponse.create({champions});
  }

  @GrpcMethod('Rpc', 'GetBattleField')
  async getBattleField(req: IEmpty): Promise<rpc.GetBattleFieldResponse> {
    const battleField = this.battleFieldService.getBattleField();
    return GetBattleFieldResponse.create({battle_field: battleField});
  }
}

ここで必要なのは単純な型情報だけなので、自分で get-champion-request.dto.ts みたいな interface を定義して使ってもいい。リクエストに含まれるパラメータが Nest.js が自動で格納するので、req.champion_id のように参照することができる(keepCase: false にするとキャメルケースになる)。

rpc.proto

protobuf のサービス定義はこんな感じ。サンプルでしかないけど League of Legends のチャンピオン取得ができる API みたいなのを想定している。

service Rpc {
  rpc GetChampion (GetChampionRequest) returns (GetChampionResponse);
  rpc ListChampions (Empty) returns (ListChampionsResponse);
  rpc GetBattleField (Empty) returns (GetBattleFieldResponse);
}

疎通テスト

疎通確認には golanggRPCurl を使うと便利。

$ grpcurl -d '{"champion_id": 1}' -plaintext -proto ./rpc/rpc.proto -import-path ./protos 127.0.0.1:5000 rpc.Rpc/GetChampion
{
  "champion": {
    "championId": 1,
    "type": "ASSASSIN",
    "name": "Akali",
    "message": "If you look dangerous, you better be dangerous."
  }
}

フルソースコード

Node.js の gRPC サンプルはあまりないので参考になるとよいです。