Nest.js ではアプリケーションサーバと同時に gRPC サーバとして起動し、別のマイクロサービスと通信できる設計になっている。
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.proto
今回サンプルとして用意した Protobuf のサービス定義は League of Legends のチャンピオン取得ができる API みたいなのを想定している。
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
にするとキャメルケースになる)。
@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 オブジェクトをそのまま型変換できるメソッドがあるので便利。
#! /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 を使うと便利。
$ 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 実装はあまりみないので参考になるとよいです。