EnvoyなしにgRPC-webをNext.js+tonic(Rust)で実現する方法

この投稿は、Rustのtonicを用いてEnvoyなしでもgRPC-webに対応したgRPCサーバーを作り、Next.jsからgRPCのエンドポイントを呼び出すまでの方法をステップ・バイ・ステップで解説するチュートリアル記事です。

gRPC-webを実現しようとすると、クライアントサイドとサーバーサイドの間に、Envoyなどのリバースプロキシが必要となります。なぜこれが必要かというと、gRPCサーバーはHTTP2なのに対し、ブラウザがHTTP2をサポートしているとは限らないというギャップを埋めるためです。EnvoyがHTTP1でgRPCリクエストを受け付け、HTTP2に変換したものをサーバーサイドに送るという仲介役を引き受けてくれます。

![gRPC-web with Envoy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/833f42d7-dfc3-08df-6655-0eaa37636ed1.png)

しかし、RustのgRPCサーバーtonicは、それ自体HTTP1とgRPC-webをサポートしているので、Envoyなどのミドルウェアを導入する必要がありません。この投稿では、tonicのgRPC-webを用いることで、EnvoyないしにgRPC-webを実現する方法を解説していきます。

![gRPC-web without Envoy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/eadb0429-75af-fa43-b9ba-c8101a0744a6.png)

なお、このチュートリアルで作ったアプリケーションのコードは[GitHub](https://github.com/suinplayground/grpc-web-with-tonic-and-next.js)にて公開しています。完成形のコードを確認したい方は、こちらもご覧ください。

## プロジェクトを作る

まず、このチュートリアルの置き場を作ります。

```shell
mkdir grpc
cd grpc
```

次に、その中に

- Rust製のサーバーサイドプログラムを置くディレクトリ
- Next.js製のクライアントサイドプログラムを置くディレクトリ
- Protocol Buffersのスキーマを置くディレクトリ

の3つをそれぞれ作ります。

```shell
mkdir server proto client
```

この2つを作るとプロジェクトのファイル構成は次のようになります。

```
.
├── client
├── proto
└── server
```

## Protocol Buffersのスキーマを作る

APIを定義するために、まずProtocol Buffersのファイルを作ります。今回作るAPIは、人(person)の情報を送ったら、その人の名前(name)を含んだ挨拶メッセージ(greeting message)を生成して返すシンプルなAPIです。

まず、`proto`ディレクトリを作り、その中に`greeting.proto`ファイルを作ります。

```shell
touch proto/greeting.proto
```

`greeting.proto`ファイルに書くスキーマは次のようになります。

```protobuf:proto/greeting.proto
syntax = "proto3";  
  
package greeting;  
  
service Greeter {  
  rpc SayHello (Person) returns (GreetingMessage) {}  
}  
  
message Person {  
  string name = 1;  
}  
  
message GreetingMessage {  
  string text = 1;  
}
```

## gRPC-webサーバーを作る

ここからは、gRPC-webサーバーをRustのtonicを使って組んできます。

### サーバーサイドプロジェクトをセットアップする

`server`ディレクトリに移動して、`cargo init`でプロジェクトの雛形を作ります。

```shell
cd server
cargo init .
```

### tonicをインストールする

tonicとtonic関連のパッケージをインストールします。

```shell
cargo add tonic
cargo add tonic-web
cargo add --build tonic-build
cargo add prost
cargo add tokio -F rt-multi-thread -F rt -F macros
```

- **tonic** ─ 本体。gRPCサーバーを実装するためのライブラリ。
- **tonic-web** ─ tonicをgRPC-webに対応するためのライブラリ。
- **tonic-build** ─ Protocol BuffersからRustコード(prostコード)を生成するツール。
- **prost** ─  Protocol BuffersのRust実装。

### tonicに付随して必要なものをインストール

tonicは`protobuf`が必要なのでそれをHomebrewでインストールしておきます。

```shell
brew install protobuf
```

### デバッグ用ツールをインストール

gRPC-webのサーバーをデバッグするときにGUIのgRPCクライアントがあると便利です。[BloomRPC](https://github.com/bloomrpc/bloomrpc)はgRPCのGUIクライアントで、これもHomebrewでインストールしておきます。

```
brew install --cask bloomrpc
```


### Rustのビルドスクリプトを作る

上で作った`greeting.proto`をRustのコンパイル時にRustコードに変換できるように、Rustのビルドスクリプト`build.rs`を作っておきます。

```shell
touch build.rs
```

`build.rs`の内容は次のようにします。

```rust:build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {  
    tonic_build::compile_protos("../proto/greeting.proto")?;  
    Ok(())  
}
```

この設定の内容は、`greeting.proto`をRustコードに変換するものですが、生成されるRustコードは`target`ディレクトリに格納されます。

### gRPC-webサーバーを実装する

`src/main.rs`にgRPC-webサーバーを実装するために、`main.rs`の内容を次のように変更します。

```rust:src/main.rs
use greeting::greeter_server::{Greeter, GreeterServer};  
use greeting::{GreetingMessage, Person};  
use tonic::{transport::Server, Request, Response, Status};  
use tonic_web::GrpcWebLayer;  
use tower_http::cors::CorsLayer;  
  
mod greeting {  
    // greeting.protoから生成したRustコードを展開するマクロ  
    tonic::include_proto!("greeting");  
}  
  
#[derive(Default)]  
struct MyGreeter {}  
  
#[tonic::async_trait]  
impl Greeter for MyGreeter {  
    async fn say_hello(  
        &self,  
        request: Request<Person>,  
    ) -> Result<Response<GreetingMessage>, Status> {  
        // gRPCリクエストから入力値を参照する  
        let name = request.into_inner().name;  
        println!("Creating a greeting message for {:?}", name);  
        // レスポンスの内容を作成する  
        let greeting_message = GreetingMessage {  
            text: format!("Hello {}!", name),  
        };  
        // gRPCレスポンスを作成する  
        let response = Response::new(greeting_message);  
        // gRPCレスポンスを返す  
        Ok(response)  
    }  
}  
  
#[tokio::main]  
async fn main() -> Result<(), Box<dyn std::error::Error>> {  
    let addr = "127.0.0.1:50051".parse().unwrap();  
    let greeter = MyGreeter::default();  
    let allow_cors = CorsLayer::new()  
        .allow_origin(tower_http::cors::Any)  
        .allow_headers(tower_http::cors::Any)  
        .allow_methods(tower_http::cors::Any);  
    println!("GreeterServer listening on {}", addr);  
    Server::builder()  
        .accept_http1(true) // gRPC-webに対応するために必要  
        .layer(allow_cors) // CORSに対応するために必要  
        .layer(GrpcWebLayer::new()) // gRPC-webに対応するために必要  
        .add_service(GreeterServer::new(greeter))  
        .serve(addr)  
        .await?;  
    Ok(())  
}
```

### gRPC-webサーバーを起動する

`cargo run`を実行してgRPC-webサーバーを起動します。

```shell
cargo run
```

これを実行すると、ビルドとサーバーの起動が行われます。しばらくして次のようなメッセージがターミナルに表示されたら、サーバーの起動が成功しています。

> GreeterServer listening on 127.0.0.1:50051

### BloomRPCでサーバーをテストする

サーバーが起動したら、greeting APIが使えるかgRPCのGUIクライアントでリクエストを出してみてテストします。

1. BloomRPCが起動する。
2. プラスボタンを押してProtocol Buffersファイル`greeting.proto`を読み込みます(1)。
3. 左メニューにAPIリストが表示されるので、その中から「SayHello」メソッドをクリックします(2)。
4. アドレス入力欄に「127.0.0.1:50051」と入力 (3)。
5. 「WEB」のトグルをonにします。(4)
6. 「Editor」のJSONを変更して、`name`プロパティに`tanaka`など好きな値にしてください。(5)
7. 実行ボタンを押す。(6)
8. 「Response」にレスポンスの内容が表示されます。(7)

![BloomGRPでgreeting APIを叩いた様子.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/7981f961-713e-d6b3-8923-18c3169ce2fa.png)

BloomRPCでリクエストを出して、レスポンスに次のような内容が表示されれば、サーバーは正しく動作していることになります。

```json
{
  "text": "Hello tanaka!"
}
```

これと同時に、ターミナルの標準出力に表示されるサーバーのログには次のようなログが表示されます。

> Creating a greeting message for "tanaka"

## クライアントを作る

ここからはNext.jsから上で作ったサーバーのAPIを呼び出せるように作っていきます。

### 必要なもの

- Yarn (このチュートリアルではv1.22.19を使用)

### クライアントサイドプロジェクトのセットアップ

ターミナルのタブを開いて、最初に作っておいた`client`ディレクトリに移動します。

```shell
cd client
```

次のコマンドを実行して、Next.jsの雛形プロジェクトを生成します。

```shell
yarn create next-app . --typescript
```

ここではインタラクティブにいろいろ聞かれますが、全部空欄で大丈夫です。これが実行され終わると、次のようなファイルが生成されます。

```
client/
├── README.md
├── next-env.d.ts
├── next.config.js
├── node_modules
├── package.json
├── pages
├── public
├── styles
├── tsconfig.json
└── yarn.lock
```

### Next.jsを起動する

次のコマンドを実行して、Next.jsを起動します。

```shell
yarn dev
```

起動したら、<http://localhost:3000>をブラウザで開いてNext.jsが起動しているか確認します。

### Connect-webをインストールする

gRPC-webをクライアントサイドで手軽に使いたいので、各種ライブラリと開発ツールをインストールします。

```shell
yarn add @bufbuild/connect-web @bufbuild/protobuf
yarn add -D @bufbuild/protoc-gen-connect-web @bufbuild/protoc-gen-es
```

ここでインストールするものの内容は次のとおりです。

- ライブラリ
	- **@bufbuild/connect-web** ─ ConnectプロトコルとgRPC-webプロトコルに対応したクライアントを提供する
	- **@bufbuild/protobuf** ─ シリアライゼーションと基本的な型のいろいろを提供する
- 開発ツール
	- **@bufbuild/protoc-gen-connect-web** ─ Protocol Buffersスキーマからサービスを生成する
	- **@bufbuild/protoc-gen-es** ─ リクエストやレスポンスといった基本的な型を生成する。

### bufの設定ファイルを作る

上でインストールした開発ツールを使って、Protocol BuffersのスキーマファイルからTypeScriptのコードを生成するには、`buf.gen.yaml`という設定ファイルを作る必要があります。

```shell
touch buf.gen.yaml
```

`buf.gen.yaml`の内容は次のようにします。

```yaml
version: v1  
plugins:  
  # Protocol Buffersのmessageに対応するTypeScriptコードを生成する (_pb.tsで終わるファイル名のもの)  
  - name: es  
    out: services  
    opt: target=ts  
  # Protocol Buffersのserviceに対応するTypeScriptコードを生成する (_connectweb.tsで終わるファイル名のもの)  
  - name: connect-web  
    out: services  
    opt:  
      - target=ts  
      - import_extension=none
```

genはプラグインを足すことで、Protocol Buffersのスキーマからさまざまな言語のコードを生成できる仕組みになっています。上の例では、`es`プラグインと`connect-web`プラグインを使っていて、`services`ディレクトリにTypeScriptファイルを生成します。

ちなみに、`es`プラグイン、`connect-web`プラグインともに、TypeScriptコードではなくJavaScriptコードやTypeScriptの型定義ファイルのみを生成することもできます。その場合は、`target=js`や`target=js+dts`にします。

### Protocol BuffersからTypeScriptファイルを生成する

生成するには[`buf`](https://docs.buf.build/installation) コマンドが必要になるので、Homebrewでインストールしておきます。

```shell
brew install bufbuild/buf/buf
```

`buf generate`を実行すると、TypeScriptファイルが生成できるわけですが、bufの`es`プラグインや`connect-web`プラグインのパスが`PATH`環境変数に追加された環境で実行する必要があります。その環境を設ける方法はいくつか考えられますが、NPMスクリプトにするのが手軽です。NPMスクリプトにするには、次のように`package.json`の`scripts`フィールドに`buf generate`コマンドを足します。

```json5:client/package.json
{  
  // 中略
  "scripts": {  
    // 中略
    "gen:services": "buf generate ../proto/greeting.proto"  
  },
  // 中略
}
```

NPMスクリプトを追加したら、`yarn`でこれを実行します。

```shell
yarn gen:services
```

このコマンドを実行すると、`buf.gen.yaml`で指定した`services`ディレクトリに、次のTypeScriptファイルが生成されます。

- **greeting_connectweb.ts** ─ 内容はProtocol Buffersのserviceに対応したオブジェクト
- **greeting_pb.ts** ─ 内容はProtocol Buffersのmessageに対応したクラス

生成されたTypeScriptファイルを少し眺めてみましょう。次は、greeting_pb.tsの中身です。

```ts:client/services/greeting_pb.ts
// 中略  
import { Message } from "@bufbuild/protobuf";  
  
export class Person extends Message<Person> {  
  name = "";  
  // 中略  
}  
  
export class GreetingMessage extends Message<GreetingMessage> {  
  text = "";  
  // 中略  
}
```

ご覧のとおり、Protocol Buffersのmessageに対応したクラスが生成されています。

次は、greeting_connectweb.tsの内容です。

```ts:client/services/greeting_connectweb.ts
import { GreetingMessage, Person } from "./greeting_pb";  
import { MethodKind } from "@bufbuild/protobuf";  
  
export const Greeter = {  
  typeName: "greeting.Greeter",  
  methods: {  
    sayHello: {  
      name: "SayHello",  
      I: Person,  
      O: GreetingMessage,  
      kind: MethodKind.Unary,  
    },  
  }  
} as const;
```

これはProtocol Buffersのserviceに対応したオブジェクトが生成されています。

Next.jsからtonicのgRPCを呼び出すには、これらの生成されたTypeScriptコードを用いていきます。

### Next.jsとtonicを接続する

ここからは、Next.jsからtonicのgRPCを呼び出す処理を実装していきます。`pages/index.tsx`の内容を次のように書き換えます。コードの部分部分の解説は、インラインコメントで説明します。

```tsx:client/pages/index.tsx
import {
  createGrpcWebTransport,
  createPromiseClient,
} from "@bufbuild/connect-web";
import type { PartialMessage } from "@bufbuild/protobuf";
import { type FormEvent, useState } from "react";
import { Greeter } from "../services/greeting_connectweb";
// messageスキーマの型情報もimportすることで利用可能
import type { GreetingMessage, Person } from "../services/greeting_pb";

// gRPCクライアントの初期化
const transport = createGrpcWebTransport({
  baseUrl: "http://localhost:50051",
});
const client = createPromiseClient(Greeter, transport);

export default function Home() {
  const [name, setName] = useState("");
  const [text, setText] = useState("");

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    // リクエストメッセージのオブジェクトはPartialMessageを使うと取れます
    const person: PartialMessage<Person> = { name };
    // gRPCメソッドを呼び出す
    const greetingMessage: GreetingMessage = await client.sayHello(person);
    console.log("greetingMessage: ", greetingMessage);
    setText(greetingMessage.text);
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input
          placeholder="name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <button type="submit">submit</button>
      </form>
      <div>{text}</div>
    </>
  );
}
```

書き換えたら、ブラウザで<http://localhost:3000/>を開いてください。`name`の入力欄があるので、そこに名前を入力して、「submit」ボタンを押してみてください。すると、その下に「Hello」で始まるテキストが結果として表示されます。この処理の裏ではgRPC-webの通信が行われていて、開発ツールのネットワークでHTTPリクエスト・レスポンスを確認すると、その内容も確認できます。

![gRPC-webが実行された様子.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/9c09e5f5-45d8-82d7-773c-fb2b15dce388.png)

## おわり

以上で、Rustのtonicを用いてEnvoyなしでもgRPC-webに対応したgRPCサーバーを作り、Next.jsからgRPCのエンドポイントを呼び出すまでのチュートリアルは終わりです。意外とシンプルな構成でgRPC-webが実現できることがおわかりいただけたのではないでしょうか。

## まとめ

- Rustのtonicを使うと、EnvoyなしにgRPC-webが使える
- bufを使うと、Protocol BuffersのスキーマからTypeScriptコードが生成できる
- このチュートリアルで作ったアプリのソースコード: [GitHub](https://github.com/suinplayground/grpc-web-with-tonic-and-next.js)