Node.js gRPC フレームワーク mali.js の紹介

## 概要

Koa ライクな API を提供する Node.js 製の gRPC フレームワーク、mali.js を試してみる機会があったので、簡単に使い方を紹介します。(公式サイトには Koa ライクとありますが、Express ライクと受け取って良いでしょう)
Node.jsのgrpc実装　`grpc` パッケージのAPIをラップして簡単にしている印象があるので、手元でgRPCのmockサーバーが必要な際などに役立ちそうだと感じました。

※ 使ってみたという内容記事なので、実装上の tips や実運用でのノウハウなどに関しては触れません。

- 公式 Document
  - https://mali.js.org/
- GitHub
  - https://github.com/malijs/mali

## サーバー実装

gRPC のサーバー/クライアントを実装する際には、まずはじめに `.proto` でサービス/メッセージタイプの定義を行う必要があります。また定義された.proto ファイルをローディングする方法には以下の二種類に分けられます。

- Dynamic codegen
  - 実行時に `.proto` ファイルを解析してコード生成を行う
- Static codegen
  - 事前に `.proto` ファイルを解析してコード生成を行う( `protoc` などのコンパイラを使用する)

また、今回は以下のような `.proto` 定義を使って紹介を進めます。

```proto
syntax = "proto3";
package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayHi (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
```


こちらは、Dynamic codegen を使って mali.js で gRPC サーバーを実装した例です。

```ts
import { resolve } from "path";
import Mali from "mali";

const PROTO_PATH = resolve(__dirname, "./protos/helloworld.proto");

function sayHello(ctx: any): void {
  ctx.res = { message: `Hello ${ctx.req.name}` };
}

function sayHi(ctx: any): void {
  ctx.res = { message: `Hi ${ctx.req.name}` };
}

export function main(): void {
  const app = new Mali(PROTO_PATH, "Greeter");
  app.use({ sayHello, sayHi });
  app.start("localhost:50051");
}

main();
```

こちらは、Static codegen の例です。
まず、前提として以下の手順で、 `./static` ディレクトリに service class のコード生成を行っておきます。

```bash
yarn add -D grpc-tools grpc_tools_node_protoc_ts

grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./static \
--grpc_out=./static \
--plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
protos/helloworld.proto

grpc_tools_node_protoc \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--ts_out=./static \
protos/helloworld.proto
```

上記の手順で生成された、service 定義を読み込み全体のコードは以下のようになります。

```ts
import Mali from "mali";

import * as services from "./static/protos/helloworld_grpc_pb";
import * as messages from "./static/protos/helloworld_pb";

function sayHello(ctx: any): void {
  const reply = new messages.HelloReply();
  reply.setMessage(`Hello ${ctx.req.name}`);
  ctx.res = reply;
}

function sayHi(ctx: any): void {
  const reply = new messages.HelloReply();
  reply.setMessage(`Hi ${ctx.req.name}`);
  ctx.res = reply;
}

export function main(): void {
  const app = new Mali(services, "GreeterService");
  app.use({ sayHello, sayHi });
  app.start("localhost:50051");
}

main();
```

例えば、Node.js の grpc 標準実装である、 `grpc` を使って同様のサーバーを実装しようとした場合、最低限のコードは以下のようになります。こちらは、Dynamic codegen の例です。

```ts
import { resolve } from "path";
import grpc from "grpc";
import protoLoader from "@grpc/proto-loader";

const PROTO_PATH = resolve(__dirname, "./protos/helloworld.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function sayHello(call: any, callback: any): void {
  callback(null, { message: `Hello ${call.request.name}` });
}

function sayHi(call: any, callback: any): void {
  callback(null, { message: `Hi ${call.request.name}` });
}

function main(): void {
  const server = new grpc.Server();
  server.addService(hello_proto.Greeter.service, {
    sayHello: sayHello,
    sayHi: sayHi
  });
  server.bind("0.0.0.0:50051", grpc.ServerCredentials.createInsecure());
  server.start();
}

main();
```

mali.js の Dynamic codegen の例と比較すると多少冗長なのが見て取れるかと思います。
mali.js では、`Mali` のコンストラクタで `.proto` ファイルのパスを渡すだけで gRPC サーバーの基本的な設定を行うことができます。
また、server に対して service の method を追加する方法も、koa で middleware を登録するように `app.use()` の API を使って行うのも特徴的です。

## 参考

- https://mali.js.org/
- https://github.com/malijs/examples