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

[Nest.js](https://github.com/nestjs/nest) ではアプリケーションサーバと同時に gRPC サーバとして起動し、別のマイクロサービスと通信できる設計になっている。

* [@nestjs/microservices/grpc](https://docs.nestjs.com/microservices/grpc)

## main.ts

```javascript
(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](https://www.npmjs.com/package/@grpc/proto-loader) が使われている。

## rpc.proto

今回サンプルとして用意した Protobuf のサービス定義は League of Legends のチャンピオン取得ができる API みたいなのを想定している。

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

## rpc.controller.ts

Controller はデコレーターで gRPC のサービス名とメソッド名を指定する方式。  
リクエストに含まれるパラメータを Nest.js が自動でオブジェクトに格納するので、`req.champion_id` のように参照することができる（オプションで `keepCase: false` にするとキャメルケースになる）。

```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});
  }
}
```

リクエスト／レスポンスの型情報はコード生成している。  
公式の `grpc-tools` よりも `protobuf.js` で生成した方が JavaScript オブジェクトをそのまま型変換できるメソッドがあるので便利。

```bash
#! /bin/bash
cd $(dirname $0)/../

SRC_DIR=./protos
DEST_DIR=./codegen

node_modules/.bin/pbjs \
  --target static-module \
  --wrap commonjs \
  --keep-case \
  --path ${SRC_DIR} \
  --out ${DEST_DIR}/rpc.js \
  ${SRC_DIR}/**/*.proto

node_modules/.bin/pbts \
  --out ${DEST_DIR}/rpc.d.ts \
  ${DEST_DIR}/rpc.js
```

なお、ここで必要なのは単純な型情報だけなので、自分で `get-champion-request.interface.ts` みたいな interface を定義して使ってもいい。

## 疎通テスト

疎通確認には golang 製の [gRPCurl](https://github.com/fullstorydev/grpcurl) を使うと便利。

```bash
$ 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."
  }
}
```

## フルソースコード

* [jnst/x-nestjs-grpc](https://github.com/jnst/x-nestjs-grpc)

Node.js の gRPC 実装はあまりみないので参考になるとよいです。