gRPCをTypeScriptで使う（前編）

gRPCは様々な言語に公式に対応していますが、残念ながらTypeScriptには対応していません。
PureなJavaScriptはIDEの恩恵が十分に得られないなど不満があったので、TypeScriptでgRPCを扱う方法を調査しました。

## 環境構築手順
#### 前提
Node.jsがインストールされていること

### 1. プロジェクト作成とパッケージインストール
以下のコマンドを実行し、プロジェクトの作成と必要なパッケージをインストールします。

```bash
npm init -y
npm install @grpc/grpc-js google-protobuf @types/google-protobuf typescript ts-node
npm install --save-dev grpc-tools grpc_tools_node_protoc_ts 
```
#### それぞれのパッケージの概要

- @grpc/grpc-js
    - gRPCのJavaScript用パッケージ。以前はgrpcというパッケージで提供されていたらしく、古いサイトやChatGPTはgrpcパッケージを使用したサンプルコードを出してくるので注意。
- grpc-tools
    - protoからJavaScriptコードを生成するためのprotocが同梱されています。
- grpc_tools_node_protoc_ts
    - protocでTypeScriptを出力するための3rdパーティ製プラグイン。grpc-toolsとgrpc_tools_node_protoc_tsはビルド時のみ必要で実行時には不要なのでdevDpendenciesとしてインストールします。
- google-protobuf
    - TimestampやAny型などよく使う型のprotoおよびそれらを表すjsファイルが含まれている（node_modules/google-protobuf/google/protobuf内参照）。また、自動生成されたソースコード内でも参照されています。
- @types/google-protobuf
    - 上記の通り、google-protobufはjsファイルで定義されているのでそのままでは型情報がTypeScriptから参照できません。@types/google-protobufをインストールすることで型情報を参照できるようになります。
- ts-node
    - TypeScriptをJavaScriptにトランスパイルすることなく直接Node.js上で扱えるようにするライブラリ。
- typescript
    - いわずとしれたJavaScriptのスーパーセット。

### 2. protoファイル作成
プロジェクト直下にprotoフォルダを作成し、greet.protoファイルを作成します。
```protobuf:greeter.proto
syntax = "proto3";
package greet;

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

message HelloRequest {
  string name = 1;
}

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

### 3. package.jsonのscriptsブロックにスクリプト追加
```json
{
  "scripts": {
    "pregenerate": "rm -rf generate && mkdir generate",
    "generate": "grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./generate --grpc_out=grpc_js:./generate --ts_out=grpc_js:./generate -I ./proto ./proto/*.proto",
    "start": "ts-node src/server.ts"
  }
}
```
npm run generateコマンドを実行することでprotoファイルからtsファイルおよびjsファイルを自動生成することができます。
pregenerateコマンドを定義することによって、generateスクリプト実行前に生成フォルダの作成も自動で実行されます。
startはサーバーを起動させるスクリプト。後ほど使います。
今回は簡便のためscriptsに直接コマンドを記述しましたが、運用を考えるならMakefileに切り出して、Makefileをscriptsから呼び出すようにしたほうが良いと思います(Makefileならコメントも書けるし、可読性も高く記述できので。[メルカリはそうしてるようです](https://engineering.mercari.com/blog/entry/20201216-53796c2494/#:~:text=%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%8C%E5%A4%9A%E3%81%8F%E3%81%A6%E8%A4%87%E9%9B%91%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%8C%E3%81%A1%E3%81%AA%E3%81%AE%E3%81%A7%20Makefile%20%E3%82%92%E4%BD%BF%E3%81%84%E3%81%BE%E3%81%99%E3%80%82))。

### 4. gRPCサーバ実装
server.tsファイルを作成し、次のコードを追加します。
```src/server.ts
import * as grpc from '@grpc/grpc-js';
import { GreeterService, IGreeterServer } from '../generate/greeter_grpc_pb';
import { HelloRequest, HelloReply } from '../generate/greeter_pb';

//protoで定義したServiceを実装するクラス定義。今回はサンプルなのでmainと同じファイル内に同居。
class GreeterServer implements IGreeterServer {
  [name: string]: grpc.UntypedHandleCall;
  sayHello(call: grpc.ServerUnaryCall<HelloRequest, HelloReply>, callback: grpc.sendUnaryData<HelloReply>): void {
    const reply = new HelloReply();
    reply.setMessage('Hello ' + call.request.getName());
    //第一引数がエラーオブジェクト。第二引数がレスポンスオブジェクト。
    callback(null, reply);
  }
}

//サーバーを起動する。
function main() {
  const server = new grpc.Server();
  server.addService(GreeterService, new GreeterServer());
  //本番運用するときはTLSで通信するべきなので、createInsecure()ではなくcreateSsl()を使う
  server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
    server.start();
    console.log('Server started on port 50051');
  });
}

main();
```

### 5. gRPCクライアント実装
client.tsファイルを作成し、次のコードを追加します。
```src/client.ts
import * as grpc from '@grpc/grpc-js';
import { GreeterClient } from '../generate/greeter_grpc_pb';
import { HelloRequest } from '../generate/greeter_pb';

function main() {
  const client = new GreeterClient('localhost:50051', grpc.credentials.createInsecure());

  const request = new HelloRequest();
  request.setName('World');

  client.sayHello(request, (error, response) => {
    if (error) {
      console.error(`Error: ${error.message}`);
    } else {
      console.log(`Greeting: ${response.getMessage()}`);
    }
  });
}

main();
```

### 6. サーバとクライアントのリクエスト
npm run startコマンドでサーバーを起動し、別のターミナルでnpm run clientを実行することで疎通確認ができます。

また、grpcurlなどのツールを使用して疎通することもできます。
```shell
grpcurl -plaintext -d '{"name": "hoge"}' -import-path . -proto proto/greet.proto localhost:50051 greet.Greeter/SayHello
```

実行結果の例:
```shell
$ npm run client

> node_grpc_gpt4_3@1.0.0 client
> ts-node src/client.ts

Greeting: Hello hoge

```
これで、基本的な環境構築はできました。

次回[応用編](https://qiita.com/noitatoCx0/items/2214198aaa9dc454ea3f)に続きます。
応用編ではメタデータの設定の仕方や、エラーレスポンスの返し方、bytes型の使い方、google-protobufで事前定義されている型の使い方などを書いています。