Fresh x Hono のつなぎ込みとデータの受け渡し

先日、「DenoとHonoでWebAuthnを使ったログインを実装する」というLTを見せていただいた。

Fresh x Hono の組み合わせについて興味を持ったので、自分でも試してみた。

参考

実装

基本

標準 Request => 標準 Response の形式をFreshとHonoが取るので、相性いいことは先のLTで学んだ。

これを使うと、次のようなAPIとIslandsを構築できる。

deno.json(抜粋)
1
2
3
4
5
6
7
8
9
{
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.6.5/",
"preact": "https://esm.sh/preact@10.19.2",
"preact/": "https://esm.sh/preact@10.19.2/",
"@hono/": "https://deno.land/x/hono@v4.0.10/",
},
}

routes/api/[...path].ts
1
2
3
4
5
6
7
8
9
10
import { Handler } from "$fresh/server.ts";
import { Hono } from "@hono/mod.ts";

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

export const appRoute = app.get("/hello_world", (c) => {
return c.json({ text: "Hello World!" });
});

export const handler: Handler = app.fetch;
islands\HelloWorld.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useEffect, useState } from "preact/hooks";
import { hc } from "@hono/mod.ts";
import { appRoute } from "../routes/api/[...path].ts";
const client = hc<typeof appRoute>("/");

async function getStatus() {
const res = await client.api.hello_world.$get();
return await res.json();
}

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

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

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

とこんな感じで、APIからメッセージを拾って表示できる。
hono(RPC)は、hono/openapiを噛ませてもできる様子だが、エディタがうまく解釈できなかった(自分が悪いはある)。
なので、ここではスキップする。

FreshのcontextからHonoへのデータ渡し

APIはHonoの領分として処理するのは楽だが、現状Freshミドルウェアで設定したContextのStateをHonoに渡すことはできていない。
それは、ContextはRequestにデータが乗らないから。
これは、以下のように実装できた。

routes/_middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
import { FreshContext } from "$fresh/server.ts";

interface State {
data: string;
}

export async function handler(
_req: Request,
ctx: FreshContext<State>,
) {
ctx.state.data = "middleware data";
return await ctx.next();
}
routes/api/[...path].ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Handler,FreshContext } from "$fresh/server.ts";
import { Hono } from "@hono/mod.ts";

interface FromFreshState {
data:string
}
const app = new Hono().basePath("/api");

export const appRoute = app.get("/hello_world_with_message", (c) => {
return c.json({ text: `Hello World with '${c.env?.data}'` });
})

export const handler: Handler = async (req, context: FreshContext<Record<string, unknown>>) => {
return await app.fetch(req, {data: context.state.data});
};

このようにすると、返却される文字列は、Hello World with 'middleware data' になる。

なお、env へのデータ設定が適切なものであるという資料は見当たらなかったのであしからず。

これができれば、Freshのレイヤーでログイン管理処理して、Honoでその他クエリを処理に使用できる。

HonoからFreshのcontextからへのデータ渡し

また、Hono => Fresh にContextで何かを返すなら、honoの処理の返却はResponse なのでHeaderに詰めて返すことができる。
ResponseからFreshのContextに値を渡し直す。

routes/_middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { FreshContext } from "$fresh/server.ts";

interface State {
data: string;
honoData: string;
}

export async function handler(
_req: Request,
ctx: FreshContext<State>,
) {
ctx.state.data = "middleware data";
const res = await ctx.next();
// Honoから帰ってきた情報をもとに処理、今回は改めてHeaderにつけ直し
res.headers.set("server", ctx.state.honoData);

return res;
}
routes/api/[...path].ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { FreshContext, Handler } from "$fresh/server.ts";
import { Hono } from "@hono/mod.ts";

interface FromFreshState {
data: string;
}
const app = new Hono().basePath("/api");

export const appRoute = app
.get("/hello_world_with_context", (c) => {
 // Headerに情報を追加する。
c.header("honoData", "hono data");
return c.json({ text: `Hello World with '${c.env?.data}'` });
});


export const handler: Handler = async (
req,
context: FreshContext<Record<string, unknown>>,
) => {
const res = await app.fetch(req, { data: context.state.data });

// HonoのResponseのHeader => context に値を移動
context.state.honoData = res.headers.get("honoData");
// HonoのResponseのHeader自体は削除
res.headers.delete("honoData");

 return res;
};

というわけで、Freshに組み込んだHonoとFreshの間での情報やり取りについてメモがてらまとめた。
ボイラープレートのような形で、各ファイルに書く必要が低減してスッキリ書けるという良さがあった。
とてもいい。

先のLTで、Fresh x Hono の熱が高まったので次に開発したアプリでも採用した。

では。