Hono RPCとSvelteKitの併用について
SvelteKit の Endpoint における型安全をめぐる考察
SvelteKit で Endpoint を書く場合、現行の実装ではそれを呼び出すためには fetch 関数を用いることになる。
fetch 関数を用いるということはつまり、Endpoint の URL や request 時の引数を渡す部分は型安全が担保されない。
そのため、個人的には「標準の方法で Endpoint を設計+fetch」はなるだけ使いたくないなと思っている。
この問題を解決するライブラリの候補は以下の通り
- tRPC
- Hono RPC
- Garph + GQty
どれを使うべきかは以下のスレッドを読んでいただきたい。
で、JS/TS で完結する場合は tRPC が筋がいいのだが、他の言語から叩くとなると Garph(GraphQL)もしくは Hono(Rest)が候補に上がる。
Garph に関しては試したところ、まだ GQty の実装が成熟していないので今後に期待かなというところ。
(つまり GraphQL Schema は定義ができるんだが、tRPC like に呼び出す部分がまだ)
なので、JS の世界では型安全に Endpoint を呼び出し、その他の言語からも通常の API として叩けるようにするには、現状 Hono が最適解だと考えている。
Hono RPC + SvelteKit + Cloudflare の落とし穴
Hono RPC についての解説は以下をご覧いただきたい。
kosei28 さんの記事通りに実装すれば SvelteKit で Hono が動くようになる。
Node.js などで動かす場合はこれで完璧であろう。
が、Cloudflare Pages/Workers で動かすときには注意が必要なので、差分をメモがわりに以下に示す。
Route を作る
kosei28 さんの記事ではhooks.server.ts
でルートの分岐を書いている。
import type { Handle } from "@sveltejs/kit";
import { app } from "$lib/hono";
export const handle: Handle = async ({ event, resolve }) => {
if (event.url.pathname.startsWith("/api")) {
return await app.handleEvent(event);
}
return resolve(event);
};
しかし実際に wrangler でこれを動かすと、ルートが存在しない旨のエラーが出る。
なので、実際にルートを定義してやる必要がある。
import { app } from "$lib/api/server.ts";
export const GET = async (event) => {
return await app.handleEvent(event);
};
export const POST = GET;
export const PUT = GET;
ちなみにこのエラーは Hono に限らず、yoga でも tRPC でも出たので、hook で処理するのは悪手かもしれない。
env や context を SvelteKit 側から Hono へ渡す
Hono ではexecutionCtx
やenv
を通して Cloudflare の KV や R2、D1、環境変数にアクセスできる
こんな感じ
import { Hono } from 'hono';
const app = new Hono();
app.post('/', async (c) => {
const key = 'example_key' as const satisfies string;
const result = c.env.BUCKET.get(key);
c.executionCtx.waitUntil(
c.env.KV.put(key, data);
)
return c.jsonT({result}, 200);
};
だけど、SvelteKit のload
関数からapp.handleEvent
を使って hono に event を渡してしまうと、hono からはenv
やcontext
にうまくアクセスができない。
なので、上記の+server.ts
関数を修正して、うまくenv
やcontext
を渡してやる必要がある。
ところで、SvelteKit の load 関数ではenv
やcontext
にアクセスするときにはplatform
変数を経由することになっている。
つまりplatform
変数から必要な情報を取り出して hono 側にどうにかして渡してやれば良いのである。
どうするって?
app.handleEvent
の代わりにapp.fetch
を使う。
app.fetch
だと、request
を渡すときに、env
や context
も明示的に渡せるのだ[1]。
まあここら辺の違いはソースを実際に読んでみるのが早いと思う。
というわけで書き換えた+server.ts
がこちら
import { app, type HonoBindings } from "$lib/api/server.ts";
export const GET = async ({ request, platform }) => {
const Env = {
...platform?.env,
...(platform?.caches ? { caches: platform.caches } : {}),
} as const satisfies HonoBindings;
return await app.fetch(request, Env, platform?.context);
};
export const POST = GET;
export const PUT = GET;
ちゃんと補完が効くようにHonoBindings
を定義しておく(定義は以下のコード参照)。
このHonoBindings
を Hono の app 初期化時に渡してやれば、Hono でルートを定義するときにも型補完がでてうまくいく。
ついでに、API を叩くためのhc
を hooks.server.ts
で定義しておくと、locals
経由で他のルートにあるload
関数からアクセスできるようになる。
import { Hono } from "hono";
export type HonoBindings = Partial<
App.Platform["env"] & { caches: App.Platform["caches"] }
>;
export const app = new Hono<{ Bindings: HonoBindings }>().basePath("/api");
const route = app.get("/hello", async (c) => {
const result = c?.env?.BUCKET?.get("hello");
return c.jsonT({ result });
});
export type AppType = typeof route;
import type { AppType } from "./server";
import { hc } from "hono/client";
export function getClient({ fetch = globalThis.fetch } = {}) {
return hc<AppType>("/api", { fetch });
}
import { getClient } from "$lib/api/client";
import { User } from "$lib/type";
export const handle = async ({ event, resolve }) => {
/** hookのなかで認証系のAPIを叩いたりすると無限ループに陥るので、即resolveする */
if (event.url.pathname.startsWith("/api")) {
return resolve(event);
}
event.locals.honoClient = getClient({ fetch: event.fetch });
/** ... */
return resolve(event);
};
まとめ
Happy Coding!
質問等あればコメントにどうぞ。
-
念の為触れておくと、
app.handleEvent
を使ってもplatform
は取り出すことができる。なぜならapp.handleEvent
は引数のevent
をc.executionCtx
に格納するため。つまりc.executionCtx?.platform?.env?.BUCKET
のようにしてアクセスすることは一応できる。ただ、Hono っぽくない ↩︎
Discussion
この記事の内容をsvelte online meetupで話したので併せてご覧ください
記事の紹介ありがとうございます!
Workersでは試したことないので分かりませんが、自分はPagesで
hooks.server.ts
を使ってtRPCとHonoを問題なくルーティングできています。(自分の記事そのままだと、型が壊れてしまっていたので後で直します・・・)
ありがとうございます。
Pagesで動いてましたか!?
自分が試したときにはエラーになっていたので妙ですね。
原因が別だったらまたここで報告します。
@kosei28 さんの記事でHono RPCを知ることになったので、とても感謝しています。
ありがとうございます!
記事更新して、実際にPagesで動かしてるデモも作ったので参考になれば!
ありがとうございます。
自分の環境では今の実装がベストなのでこのまま行こうと思います
レポジトリありがとうございます
ちなみにエラーの再現なんですが、
作っていただいたこのレポジトリ上で
adapterをこちら→https://kit.svelte.jp/docs/adapter-cloudflare に変更
としてみてください。
これはwranglerを用いたpreview環境なのですが、何度か試しているとworkerからfetchのエラーが出てしまいますね。
幸い、作っていただいたレポジトリにおいては表示の影響はなさそうです。
ただ、自分の作っているアプリでは、
hooks.server.ts
でrouteを捌く実装の場合、このエラーによってたまに画面が応答しなくなったり、外部の環境からrestとしてapiを呼び出した場合にうまく応答しなかったことがありましたちょっと追記事項が必要なので、再現のためのレポジトリを作ります
なるほど…
monorepoでgithub actionsからデプロイするためにadapter-cloudflareを使ってるプロジェクトもあるんですが、wranglerでローカルでテストしてなかったので知りませんでした
参考になる情報をありがとうございます!
はい、そうなんです
KVやR2といったCloudflareの機能をローカルで試すためにはwrangler必要なんですよね
ありがとうございます