😎

WireitでHonoRPCなモノレポの↑開発体験向上↑

2024/12/18に公開

Wireit で HonoRPC なモノレポの開発体験向上

この記事は、 inaridiy がお送りする、Hono Advent Calendar 2024 年の 18 日目の記事です。

導入

さて突然ですが、皆さんは HonoRPC を使っているでしょうか?
ちなみに HonoRPC とは、Hono 製の REST API を型安全に呼び出せる便利なヘルパーです。
tRPC に似ていますが、HonoRPC の特徴は、シンプルに REST API を書くだけで型補完が効く点にあります。
https://zenn.dev/yusukebe/articles/a00721f8b3b92e

僕はよくモノレポ構成のプロジェクトで Cloudflare Workers の Service Bindings をするときや、フロントエンドから HonoJS のバックエンドを叩く時などに使っています。めちゃくちゃ便利ですよね

しかし、HonoRPC をそのまま使うと Typescript の型推論コストが高く、コードの成長に合わせてエディタ―の補完が遅くなってしまいます。このままだと開発体験が損なわれるので、公式ドキュメントでは型情報の事前ビルドが推奨されています。

https://hono.dev/docs/guides/rpc#compile-your-code-before-using-it-recommended

この手順でエディタ補完問題は解消できますが、バックエンド側を修正するたびに型情報をビルドし直すのはなかなか面倒です。

導入が長くなりましたが、記事では、Google 製の wireit を用いて、このビルドプロセスを自動化するとともに、モノレポ内の他パッケージビルドもいい感じに一元管理する方法を紹介します。

サンプルコード

サンプルとして、シンプルな Todo アプリを実装しました。フロントエンドとバックエンドの両方が Cloudflare Workers 上で動作し、フロントエンドからバックエンドへはService Bindingsを通じてHonoRPCクライアントを使って問い合わせます。

https://wireit-demo-frontend.inaridiy.workers.dev/

https://github.com/inaridiy/demo-wireit

Wireit とは

Wireit は、Google 製のビルドツールで、npm スクリプトの依存関係を package.json 内で宣言的に管理できるツールです。
また、各スクリプトは入力ファイルと出力ファイルを元にキャッシュしてくれるので、無駄なビルドを省くことができます。

例えば、dev コマンドを実行する前に、バックエンドの HonoRPC の型情報をビルドしておきたいと思ったとき、以下のように記述すれば dev コマンドを実行すると HonoRPC の型情報がビルドされるようになります。

{
  "scripts": {
    "dev": "wireit"
  },
  "wireit": {
    "dev": {
      "command": "wrangler dev --port 8788",
      "service": true,
      "dependencies": ["../backend:build"]
    }
  }
}

実践的な Wireit の使い方

サンプルプロジェクトの構成

サンプルプロジェクトは以下のような構成になっています:

demo-wireit/
├── apps/
│   ├── backend/     # Cloudflare Workers上で動作するバックエンドAPI
│   │   └── package.json
│   └── frontend/    # Cloudflare Workers上で動作するフロントエンド
│       └── package.json
└── packages/
    └── shared/      # 共有の型定義やユーティリティ
        └── package.json

このプロジェクトでは:

  • バックエンドで HonoRPC のエンドポイントを定義
  • フロントエンドから HonoRPC を使用してバックエンドを呼び出し
  • 共有パッケージで共通の型定義を管理

という構成で、Wireit を使って各パッケージのビルドと依存関係を管理しています。

HonoRPC の実装とビルド

導入で述べた通り、HonoRPC のクライアントの型情報を事前にビルドすることで型推論のパフォーマンスを向上させることが出来ます。

apps/backend/src/index.ts
const app = createHonoApp().get("/api/todos", async (c) => {
  const results = await c.get("db").query.todos.findMany();
  return c.json(results);
});
// 他のエンドポイント...

// クライアント型の生成
const client = hc<typeof app>("");
export type Client = typeof client;
export const hcWithType = (...args: Parameters<typeof hc>): Client =>
  hc<typeof app>(...args);

このバックエンドコードをtscコマンドでビルドすることで、フロントエンドで使用する型情報が生成されます。
フロントエンド側では、以下のように型付きの RPC クライアントを作成できます。

apps/frontend/src/libs/backend-rpc.ts
import { hcWithType } from "backend";

export const createBackendRpc = (env: CloudflareBindings) => {
  return hcWithType("https://worker.com", {
    fetch: env.BACKEND_WORKER.fetch.bind(env.BACKEND_WORKER),
  });
};

このように型付きの RPC クライアントを作成できます。

キャッシュを活用した効率的なビルド

モノレポでの開発では、パッケージの数が増えるにつれてビルド時間も増加していきます。特に型定義の生成は、変更がないのに毎回ビルドされてしまうと開発体験が著しく低下します。

そこで Wireit のキャッシュ機能が役立ちます。バックエンドの HonoRPC クライアントや shared パッケージなど、事前にビルドが必要な箇所では、以下のような設定を使用して、コードに変更があった時だけビルドが実行されるようにしています。

apps/backend/package.json
{
  "wireit": {
    "build": {
      "command": "tsc",
      "files": ["src", "tsconfig.json", "package.json", "worker-configuration.d.ts"],
      "output": ["dist"],
      "dependencies": ["cf-typegen", "../../packages/shared:build"]
    }
  }
}

files に指定したファイルに変更があった場合のみビルドが実行され、それ以外の場合は output で指定した前回のビルド結果がキャッシュから使用されます。
何で files は複数形で、output は単数形なんだろう

複雑な依存関係の管理

Wireit の設定では、dependencies プロパティを使用してスクリプトの依存関係を宣言することで、必要なスクリプトを自動的に実行できます。また、service プロパティを設定することで、スクリプトをサーバーのような長時間実行されるコマンドとして宣言できます。

例えば、フロントエンドの開発サーバーを起動する際に、バックエンドの HonoRPC の型情報を生成したり、バックエンドの Worker を起動した後にフロントエンドの Worker を起動する、といった処理を簡潔に設定できます。

apps/frontend/package.json
{
  "wireit": {
    "dev": {
      "command": "wrangler dev --port 8788",
      "dependencies": [
        "cf-typegen",
        "../../packages/shared:build",
        "../backend:build",
        "../backend:dev"
      ],
      "service": {
        "readyWhen": {
          "lineMatches": "\\S\\[wrangler:inf\\] Ready on http://localhost:\\d+"
        }
      }
    }
  }
}

npm run devを実行すると、Wireit がいい感じに依存関係を解決してくれます。まず Cloudflare の型定義を生成し、次に共有パッケージをビルド。そしてバックエンドの HonoRPC のビルドと起動を済ませてから、最後にフロントエンドの開発サーバーが立ち上がります。

余談ですが、同時に wrangler dev コマンドを起動すると、稀にクラッシュしてしまうことがあります。wiresit では service.readyWhen プロパティを使用することで、バックエンド Worker の起動完了を待ってからフロントエンドの Worker を起動するように設定できるため、この問題も解消できます。

まとめ

大規模なモノレポで HonoRPC を使用する際は、Wireit を活用して開発体験を向上させましょう!

レッツ 快適な HonoRPC ライフ!!

Discussion