NestJSでgRPC API作るならコード生成はts-protoで決まり

## はじめに
Nest.jsはTypescriptで記述可能なNode.jsのAPIフレームワークで、gRPCをサポートしています。
そんなNest.jsでgRPC APIを実装しようとすると、`static_codegen`と`dynamic_codegen`を組み合わせて実装することになると思います。
`static_codegen`と`dynamic_codegen`については以下のブログが参考になります。
https://caddi.tech/archives/455#3

上記の記事にもあるように、`static_codegen`にはいくつか選択肢があります。
そして、記事と同じ議論でprotobuf.jsに行き着くのかなと思います。
しかし、protobuf.jsで生成されるサービスのコードは、アノテーションベースで実装するNest.jsとは相性が悪いです。
そのため、protobuf.jsを使用する場合、メッセージのインターフェースのみを使用し、サービス部分はスクラッチで実装していました。

そんなときに、**Nest.jsのサポート**を謳うツール、ts-protoを知りました。
ということで、ts-protoとNest.js + ts-protoでのgRPC APIの実装方法を紹介していきます。

## ts-proto
[ts-proto](https://github.com/stephenh/ts-proto)とは

> ts-proto transforms your .proto files into strong typed typescript files!

とあるとおり、ProtocolBuffersファイルからTypescriptファイルを生成するツールの一つです。


上でも述べたように、ts-protoでは**Nest.jsのサポート**を謳っています。激アツです。
https://github.com/stephenh/ts-proto/blob/master/NESTJS.markdown

## 使ってみる
ソースは以下です。
https://github.com/vol1003/nestjs-grpc-sample

`src`ディレクトリは、以下のような構成になっています。

```
src
├── app.module.ts
├── main.ts
├── hero
│   ├── hero.controller.spec.ts
│   ├── hero.controller.ts
│   └── hero.module.ts
└── protos
    └── hero.proto
```

### 準備
今回は、公式ドキュメントを基に、以下のProtocolBuffersファイルを例としてNest.jsでgRPC APIを実装していきます。
https://docs.nestjs.com/microservices/grpc

```protobuf
syntax = "proto3";

package hero;

service HeroesService {
  rpc FindOne (HeroById) returns (Hero) {}
}

message HeroById {
  int32 id = 1;
}

message Hero {
  int32 id = 1;
  string name = 2;
}
```

Nest.jsでgRPCを実装する準備もしておきます。

```sh
yarn add @nestjs/microservices grpc @grpc/proto-loader
```

### ts-protoでコードを生成する
まずは、ts-protoをインストールします。

```sh
yarn add ts-proto -D
```

ts-protoは、`protoc`のプラグインとして動作します。
以下コマンドで、`src/hero/hero.ts`にTypescriptのコードが生成されます。

```sh
protoc --plugin=(yarn bin)/protoc-gen-ts_proto \
  --ts_proto_out=src/hero \
  --ts_proto_opt=nestJs=true \              # Nest.js向けサービスインターフェースを出力
  --ts_proto_opt=outputClientImple=false \
  --ts_proto_opt=addGrpcMetadata=true \     # gRPCのメタデータをサービスの引数に追加
  -Isrc/protos \
  src/protos/hero.proto
```

生成されたコードは以下のようになっています。

```javascript
/* eslint-disable */
import { Metadata } from 'grpc';
import { Observable } from 'rxjs';
import { GrpcMethod, GrpcStreamMethod } from '@nestjs/microservices';


export interface HeroById {
  id: number;
}

export interface Hero {
  id: number;
  name: string;
}

export interface HeroesServiceController {

  findOne(request: HeroById, metadata?: Metadata): Promise<Hero> | Observable<Hero> | Hero;

}

export interface HeroesServiceClient {

  findOne(request: HeroById, metadata?: Metadata): Observable<Hero>;

}

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

export const HERO_PACKAGE_NAME = 'hero'
export const HEROES_SERVICE_NAME = 'HeroesService';
```

`HeroesServiceController`というコントローラーのインターフェースが生成されています。
この例では、`--ts_proto_opt=addGrpcMetadata=true`を付与しているため、引数に`metadata`が追加されています。
また、`HeroesServiceControllerMethods`というアノテーションが生成されています。
これらを活用して、`HeroController`を実装していきます。

### `HeroController`を実装する

実装は、以下のようになります。

```javascript
@Controller('hero')
@HeroesServiceControllerMethods()  // ①
export class HeroController implements HeroesServiceController {  // ②
  findOne(request: HeroById, metadata?: Metadata): Promise<Hero> | Hero {
    if (metadata) {
      console.log(metadata);
    }

    return {
      id: request.id,
      name: 'superman',
    } as Hero;
  }
}
```

#### 1. `@HeroesServiceControllerMethods()`を付与する
このアノテーションは、メソッド名から適切なgRPC関連のアノテーション（`@GrpcMethod`, `@GrpcStreamMethod`）を付与します。
サービス定義が膨らめば膨らむほど楽ができて嬉しいですね。

#### 2. `HeroesServiceController`を実装する
`HeroesServiceController`は、当該サービスのコントローラーで実装すべきメソッドが定義されているインターフェースです。
このインターフェースの存在のおかげで、proto定義とコントローラーの実装の不一致をコンパイル時点で発見できます。
これは、protobuf.jsでは受けられなかった恩恵かと思います。

また、IDEによってはインターフェースから実装すべきメソッドを自動生成する機能があります。
こういった機能を利用すれば、アノテーションの自動付与と合わせて、開発者は本当にロジックの実装に集中できますね。

### grpcurlで動作確認
grpcurlで動作確認をします。確認ポイントは、3点です。

1. 起動できるか
2. gRPC APIとして機能するか
3. metadataは受け取ることができるか

#### 1. 起動できるか
ちゃんと起動できました。

```sh
yarn start
```

```
yarn run v1.22.4
$ nest start
[Nest] 42855   - 08/01/2020, 6:40:10 PM   [NestFactory] Starting Nest application...
[Nest] 42855   - 08/01/2020, 6:40:10 PM   [InstanceLoader] AppModule dependencies initialized +9ms
[Nest] 42855   - 08/01/2020, 6:40:10 PM   [InstanceLoader] HeroModule dependencies initialized +0ms
[Nest] 42855   - 08/01/2020, 6:40:10 PM   [NestMicroservice] Nest microservice successfully started +63ms
```

#### 2. gRPC APIとして機能するか
期待通りの振る舞いです。

```sh
grpcurl -d '{"id": 5}' -plaintext \ 
  -proto hero.proto \
  -import-path ./src/protos/ \
  -rpc-header 'credentials: hogehoge' \
  localhost:5000 \
  hero.HeroesService/FindOne
```

```json
{
  "id": 5,
  "name": "superman"
}
```

#### 3. metadataは受け取ることができるか
こちらも受け取っている様子が確認できます。

```
Metadata {
  _internal_repr: {
    'user-agent': [ 'grpc-go/1.30.0-dev' ],
    credentials: [ 'hogehoge' ]
  },
  flags: 0
}
```

## まとめ
ということで、Nest.js + ts-protoの組み合わせについて紹介しました。
初期実装のコストも、変更時のコストも下がるのは、大きなメリットですね。
今のところNest.jsでgRPCを実装するならts-protoが一番適しているかと思っています。

## 余談
`proto3`では、`optional`が削除されました。
駆け出しgRPCerとしては、`optional`なくすとか正気か？という気分だったのですが、v3.12.0でoptionalがexperimental機能として電撃復活しました。
（内部的には、`oneof`でラップしているようです）
https://github.com/protocolbuffers/protobuf/blob/v3.12.0/docs/implementing_proto3_presence.md
https://github.com/protocolbuffers/protobuf/blob/v3.12.0/docs/field_presence.md

ts-protoでは、`optional`サポートの議論も行われており、期待が高まります。
https://github.com/stephenh/ts-proto/issues/73