@hono/openapi-zod と openapi2aspida で楽々 api 開発
TL;DR
今回は簡単のために自分自身にリクエストを送る方法でやったが cloudflare workers で外部 worker とやり取りするときに有用だと思われる。
これが
const handler: RouteHandler<typeof route, HonoEnv> = (c) => {
return c.env.SELF_SERVICE.fetch(new URL("/", c.req.url));
};
こうできる
const handler: RouteHandler<typeof route, HonoEnv> = async (c) => {
const res = await c.get("client").$get();
return c.jsonT(res);
};
@hono/openapi-zod と openapi2aspida を使ったら面白そう
@hono/openapi-zod は簡単に path parameter, request header, request body の validation ができる上に openapi schema もできる優れものです。今日はこれと openapi から TS の型生成を行い client を作ることができる openapi2aspida を使って自分自身にリクエストを型安全に送る機構を考えていこうと思います。
@hono/openapi-zod で schema と handler を定義する
これだけで openapi schema と api が作れます。これに対して openapi2aspida から型生成して client を作り handler 内で使用します。
import { OpenAPIHono, RouteHandler, createRoute } from "@hono/zod-openapi";
import { z } from "zod";
import { prettyJSON } from "hono/pretty-json";
type Bindings = {
//
};
type Variables = {
//
};
type HonoEnv = {
Bindings: Bindings;
Variables: Variables;
};
const route = createRoute({
path: "/",
method: "get",
description: "@hono/openapi-zod はすごい!",
responses: {
200: {
description: "成功時のレスポンスは success: true です!",
content: {
"application/json": {
schema: z.object({ success: z.literal(true) }),
},
},
},
},
});
const handler: RouteHandler<typeof route, HonoEnv> = (c) => {
return c.json({ success: true });
};
const app = new OpenAPIHono();
app.use("/doc/*", prettyJSON());
app.doc("/doc", {
openapi: "3.0.3",
info: { title: "openapi doc", version: "0.0.1" },
servers: [{ url: "/api" }],
});
app.openapi(route, handler);
export default app;
/doc の内容
自分自身にリクエストを送る client を作る
まず service bindings を wrangler.toml にかき、Hono から参照できるように Bindings の型を変更します。cloudfalre workers はほかの workers にリクエストを送るには service bindings が必要ですが、自分自身にリクエストを送るときも必要です。
+ services = [
+ { binding = "SELF_SERVICE", service = "openapi-hono", environment = "production" }
+ ]
type Bindings = {
- //
+ SELF_SERVICE: Fetcher;
};
つぎに openapi2aspida の設定をしていきます。まず aspida.config.cjs を作ります。
module.exports = {
input: "src/config/api",
openapi: {
inputFile: "http://localhost:8787/doc",
},
};
wrangler dev
をしている状態で npx openapi2aspida -c aspida.config.cjs
を実行。以下のような内容のファイルが出力されます
config/api の出力内容
$api.ts
import type { AspidaClient, BasicHeaders } from "aspida";
import type { Methods as Methods_by08hd } from ".";
const api = <T>({ baseURL, fetch }: AspidaClient<T>) => {
const prefix = (baseURL === undefined ? "http://localhost/api" : baseURL).replace(/\/$/, "");
const GET = "GET";
return {
/**
* @hono/openapi-zod はすごい!
* @returns 成功時のレスポンスは success: true です!
*/
get: (option?: { config?: T | undefined } | undefined) =>
fetch<Methods_by08hd["get"]["resBody"], BasicHeaders, Methods_by08hd["get"]["status"]>(
prefix,
"",
GET,
option,
).json(),
/**
* @hono/openapi-zod はすごい!
* @returns 成功時のレスポンスは success: true です!
*/
$get: (option?: { config?: T | undefined } | undefined) =>
fetch<Methods_by08hd["get"]["resBody"], BasicHeaders, Methods_by08hd["get"]["status"]>(prefix, "", GET, option)
.json()
.then((r) => r.body),
$path: () => `${prefix}`,
};
};
export type ApiInstance = ReturnType<typeof api>;
export default api;
index.ts
/* eslint-disable */
export type Methods = {
/** @hono/openapi-zod はすごい! */
get: {
status: 200;
/** 成功時のレスポンスは success: true です! */
resBody: {
success: true;
};
};
};
client を作る
ここまでできたところで client を生成する関数を作ります。これを middleware として実装し、hono の variables として登録して handler 内で使います。
import aspida, { FetchConfig } from "@aspida/fetch";
import api from "./config/api/$api";
const createClient = (env: Bindings, config?: FetchConfig) =>
api(aspida((...args: Parameters<typeof fetch>) => env.SELF_SERVICE.fetch(...args), config));
Variables の内容を変更
type Variables = {
- //
+ client: ReturnType<typeof createClient>;
};
variable にセットする middleware を作る
baseURL だけ注意が必要です。c.req.url をそのまま渡してしまうと client が実行される handler ごとに baseURL が変わってしまうので new URL("/", c.req.url).href
で固定します。
const clientMiddleware: MiddlewareHandler<HonoEnv> = (c, next) => {
const client = createClient(c.env, {
baseURL: new URL("/", c.req.url).href,
});
c.set("client", client);
return next();
};
これを app.use("/doc/*", prettyJSON())
の上あたりにおいておけばよいです。
app.use("*", clientMiddleware);
新しい endpoint を作って自分自身にリクエストを送ってみる
const route2 = createRoute({
path: "/route2",
method: "get",
description: "/ の内容をそのまま返します",
responses: {
200: {
description: "成功です",
content: {
"application/json": {
schema: z.object({ success: z.literal(true) }),
},
},
},
},
});
const handler2: RouteHandler<typeof route2, HonoEnv> = async (c) => {
const res = await c.get("client").$get();
return c.jsonT(res);
};
app.openapi(route2, handler2);
できた
~/openapi-hono ❯❯❯ curl http://localhost:8787/route2
{"success":true}%
これをやると service binding のテストが行いたくなる
Discussion