LoginSignup
12
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-01

はじめに

Nest.jsはTypescriptで記述可能なNode.jsのAPIフレームワークで、gRPCをサポートしています。
そんなNest.jsでgRPC APIを実装しようとすると、static_codegendynamic_codegenを組み合わせて実装することになると思います。
static_codegendynamic_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とは

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

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を実装する準備もしておきます。

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

ts-protoでコードを生成する

まずは、ts-protoをインストールします。

yarn add ts-proto -D

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

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

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

/* 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を実装する

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

@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. 起動できるか

ちゃんと起動できました。

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として機能するか

期待通りの振る舞いです。

grpcurl -d '{"id": 5}' -plaintext \ 
  -proto hero.proto \
  -import-path ./src/protos/ \
  -rpc-header 'credentials: hogehoge' \
  localhost:5000 \
  hero.HeroesService/FindOne
{
  "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

12
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5