[Deno] FreshでHonoのRPCモードを使う 🍋+🔥

# 🍋Freshについて

FreshはDeno社製のWebフレームワークです。

- [Fresh](https://fresh.deno.dev/)
- [[Deno] Fresh入門ハンズオン ～ プロジェクト作成からデプロイまで](https://qiita.com/access3151fq/items/772cd0a22d7eea8d4d03)

Next.jsのようなファイルシステムベースのルーティングを採用していますが、デフォルトではPreactによるSSRとなっており、必要な場合のみクライアントサイドJSが配信される「アイランドアーキテクチャ」を採用しています。
また、ルーティングにはWeb標準のRequest/Responseオブジェクトを使用します。

# 🔥Honoについて

Honoはexpressライクなサーバーサイドフレームワークです。Web標準Request/Responseに対応しており、サーバー/クライアント間で型を共有できる「RPCモード」に対応しています。

- [Hono](https://hono.dev/)
- [Hono[炎]っていうイケてる名前のフレームワークを作っている](https://zenn.dev/yusukebe/articles/0c7fed0949e6f7)

# FreshとHonoを組み合わせる

FreshではJSONなどのレスポンスを返す際、通常`routes/`ディレクトリ内のファイルに`handler`関数を設定します。

```ts:routes/api/hello.ts
import { Handler } from "$fresh/server.ts";

// この部分をHonoに置き換えたい
export const handler: Handler = (req, ctx) => {
  return Response.json({ hello: "world" });
};
```

この`handler`関数の部分をHonoに処理させて、ルーティングをカスタマイズしていきたいと思います。

### 1. import-mapの設定

Honoを使う前に、`import_map`にHonoの読み込み先とバージョンを設定しておきます。
この設定をしておくと、import文に`https://deno.land/x/hono@v3.2.1/mod.ts`と書く代わりに`$hono/mod.ts`と書くことができます。

```diff_json:import_map.json
  {
    "imports": {
      "$fresh/": "https://deno.land/x/fresh@1.1.5/",
      "preact": "https://esm.sh/preact@10.13.1",
      "preact/": "https://esm.sh/preact@10.13.1/",
      "preact-render-to-string": "https://esm.sh/*preact-render-to-string@5.2.6",
      "@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
      "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
      "twind": "https://esm.sh/twind@0.16.19",
      "twind/": "https://esm.sh/twind@0.16.19/",
+     "$hono/": "https://deno.land/x/hono@v3.2.1/"
    }
  }
```

### 2. Honoで処理したいパスのルーティングを設定する

Freshでは、角括弧(`[]`)を用いることで、ワイルドカードを使用したルーティングを設定することができます。

```:ルーティングの一例
routes/index.tsx          --> / (ルート)にアクセスした時に実行される
routes/hello.tsx          --> /helloにアクセスした時に実行される
routes/users/[name].tsx   --> /users/bob や /users/alice にアクセスした時に実行される
routes/hello/[...path].ts --> /hello/foo や /hello/foo/bar など、hello/以下にアクセスした時に実行される
```

`[...path]`のような形式のワイルドカードを使用すると、特定のディレクトリ以下へのリクエストをまとめて受け取ることができます。
これを利用して、今回は`/api/`以下のパスへのリクエストをHonoで受け取るようにしてみます。コードは以下のようになります。

```ts:/routes/api/[...path].tsx (例)
import { Handler } from "$fresh/server.ts";
import { Hono } from "$hono/mod.ts";

// /api/以下へのリクエストは全てこのファイルで受け取る

const app = new Hono().basePath("/api");

// ルーティングの設定
const route = app
  .get("/status", (c) => c.jsonT({ status: "ok" }))
  .get("/hello", (c) => c.jsonT({ hello: "world" }));

export const handler: Handler = (req) => app.fetch(req);
export type AppType = typeof route; // rpcモードを使用するときはこのAppTypeをexportする
```

ポイントは`.basePath("/api")`でどのパスが処理の対象になっているのか明示することです。
また、後でRPCモードを使用するため、レスポンスの作成を`.jsonT()`で行い、最後の行で`AppType`をexportしています。

上のコードを保存した状態でサーバーを起動し、`/api/hello`にアクセスすると、レスポンスが返ってくることが確認できると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/1e91621f-a19e-f036-0205-979392078646.png)


### 3. Honoで定義したAPIをRPCモードでクライアント側から呼び出す

Honoにはサーバー側で定義したエンドポイントの型定義を使用して、フロントエンド側のAPI呼び出しに型を付けることができる**RPCモード**が存在します。今回はこれを使ってみたいと思います。

:::note
HonoのRPCモードについては、詳しくは[公式ドキュメント](https://hono.dev/guides/rpc)をご覧ください。
:::

Freshではクライアント側のコードは`islands`ディレクトリか`components`ディレクトリに入れる必要があります。
今回は、`islands`ディレクトリに`Status`コンポーネントを作り、先ほどHonoで作成したAPIのエンドポイントを呼び出していきたいと思います。

RPCモードを使うには、以下のようなコードになります。


```diff_tsx:islands/Status.tsx
  import { useEffect, useState } from "preact/hooks";
+ import { hc } from "$hono/mod.ts";
+ // サーバー側から必要な型をimportする
+ import type { AppType } from "../routes/api/[...path].ts";

+ // クライアント作成時に`AppType`をジェネリクスで渡す
+ const client = hc<AppType>("/");
+ async function getStatus() {
+   // API呼び出し
+   const res = await client.api.status.$get();
+   return await res.json();
+ }

  export default function Hello() {
    const [message, setMessage] = useState("");

+   useEffect(() => {
+     getStatus().then((data) => setMessage(data.status));
+   }, []);

    return <div>{message}</div>;
  }
```

コードの説明です。
まず、先ほど`routes/api/[...path].ts`で作成した`AppType`型をimportし、`hc<AppType>("/")`のようにジェネリクスで渡します。こうすることでクライアントとサーバーで共通の型定義を使用することができます。

```diff_tsx
+ import { hc } from "$hono/mod.ts";
+ import type { AppType } from "../routes/api/[...path].ts";

+ const client = hc<AppType>("/");
```

その後、`client.api.status.$get()`関数を呼び出すとAPI呼び出しが行われます。

```diff_tsx
+ async function getStatus() {
+   const res = await client.api.status.$get();
+   return await res.json();
+ }
```

vscodeで確認すると、ちゃんと`res.json()`の戻り値にサーバー側で定義したものと同じ型が付いていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/0fa724a9-ea4d-0452-9fbe-b66794bafb87.png)

以上、FreshとHonoを組み合わせてRPCモードを使う方法の解説でした。

# まとめ

- FreshとHonoはどちらもWeb標準のRequest/Responseオブジェクトに対応しているため、組み合わせることができる
- Freshの特定のパス以下のルーティングを、Honoに処理させることができる
- RPCモードを使用して、クライアント側とサーバー側でAPI呼び出しの型定義を共有できる