SvelteKit の Endpoint における型安全をめぐる考察
SvelteKit で Endpoint を書く場合、現行の実装ではそれを呼び出すためには fetch 関数を用いることになる。
https://kit.svelte.jp/docs/routing#server
fetch 関数を用いるということはつまり、Endpoint の URL や request 時の引数を渡す部分は型安全が担保されない。 そのため、個人的には「標準の方法で Endpoint を設計+fetch」はなるだけ使いたくないなと思っている。
この問題を解決するライブラリの候補は以下の通り
- tRPC
- Hono RPC
- Garph + GQty
どれを使うべきかは以下のスレッドを読んでいただきたい。
https://twitter.com/ryoppippi/status/1664258993118740480 https://twitter.com/ryoppippi/status/1664259029227524097 https://twitter.com/ryoppippi/status/1664259247079653379
で、JS/TS で完結する場合は tRPC が筋がいいのだが、他の言語から叩くとなると Garph(GraphQL)もしくは Hono(Rest)が候補に上がる。 Garph に関しては試したところ、まだ GQty の実装が成熟していないので今後に期待かなというところ。 (つまり GraphQL Schema は定義ができるんだが、tRPC like に呼び出す部分がまだ) https://github.com/ryoppippi/garph-sveltekit
なので、JS の世界では型安全に Endpoint を呼び出し、その他の言語からも通常の API として叩けるようにするには、現状 Hono が最適解だと考えている。
Hono RPC + SvelteKit + Cloudflare の落とし穴
Hono RPC についての解説は以下をご覧いただきたい。 https://zenn.dev/kosei28/articles/f4bac1ed2b64a7
https://zenn.dev/yusukebe/articles/53713b41b906de#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 async function GET(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 も明示的に渡せるのだ^念の為触れておくと、`app.handleEvent`を使っても`platform`は取り出すことができる。なぜなら`app.handleEvent`は引数の`event`を`c.executionCtx`に格納するため。つまり`c.executionCtx?.platform?.env?.BUCKET`のようにしてアクセスすることは一応できる。ただ、Hono っぽくない。
https://hono.dev/api/hono#fetch
まあここら辺の違いはソースを実際に読んでみるのが早いと思う。 https://github.com/honojs/hono/blob/94812fcf2db49bc26dd5e421610433e7510c0529/src/hono-base.ts#L347-L353
というわけで書き換えた+server.tsがこちら
import type { HonoBindings } from '$lib/api/server.ts';
import { app } from '$lib/api/server.ts';
export async function GET({ 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 async function handle({ 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! 質問等あればコメントにどうぞ。