cloudflare/itty-router-openapi: OpenAPI 3 schema generator and validator for Cloudflare Workers https://github.com/cloudflare/itty-router-openapi
これなに
itty-router
をベースにしていて- 独自のスキーマでハンドラを実装すれば
- OpenAPI 3のスキーマを自動生成してくれる
とまあ、実装がドキュメントにそのままなる今風やつ。
特徴といえば、Cloudflare Workersで動く・・・ってところで、それで言うとHonoにもそれ用のミドルウェアがあるので、どっちを使うかは好みか。
https://github.com/honojs/middleware/tree/main/packages/zod-openapi
内部的に、@asteasolutions/zod-to-openapi
が使われてるところまで一緒。
コード例
// main.ts
import { OpenAPIRouter, OpenAPIRoute } from "@cloudflare/itty-router-openapi";
import { router as tasksRouter } from "./tasks-routes";
export type Env = { DB: D1Database };
const router = OpenAPIRouter();
// Simple
router.get(
"/api/healthcheck",
class extends OpenAPIRoute {
static schema = {
tags: ["Healthcheck"],
summary: "Healthcheck endpoint",
responses: {
"200": {
description: "Healthcheck response",
schema: { status: "ok" },
},
},
};
async handle() {
return { status: "ok" };
}
},
);
// Nested routes
router.all("/api/tasks/*", tasksRouter);
// Plain itty routes
router.all("*", () => new Response("Not Found", { status: 404 }));
export default { fetch: router.handle };
コードの書き味としては、とにかく既存のitty-router@v4
プロジェクトを壊さないようになってるのが特徴的だった。
OpenAPIRouter
をRouter
から差し替えるだけで動く- 既存のハンドラもそのまま動く
ただしその分、DXやらがちょっと損なわれる感じの印象。
// tasks-routes.ts
import {
OpenAPIRouter,
OpenAPIRoute,
Path,
} from "@cloudflare/itty-router-openapi";
import { z } from "zod";
import type { Env } from "./main";
export const router = OpenAPIRouter({
base: "/api/tasks",
});
const TaskSchema = z.object({
name: z.string().openapi({ example: "Buy milk" }),
slug: z.string().openapi({ example: "task:1234" }),
description: z.string().optional().openapi({ example: "My task" }),
completed: z.boolean(),
due_date: z.date(),
});
router.get(
"/:taskSlug",
class extends OpenAPIRoute {
static schema = {
tags: ["Tasks"],
summary: "Get a single Task by slug",
parameters: {
taskSlug: Path(z.string().openapi({ example: "12" })),
},
responses: {
"200": {
description: "Single Task object with metadata",
schema: {
metaData: {},
task: TaskSchema,
},
},
},
};
// XXX: Not typed :(
async handle(
req: Request,
env: Env,
ctx: ExecutionContext,
data: { params: { taskSlug: string } },
) {
const { taskSlug } = data.params;
req;
env.DB;
ctx;
return {
metaData: { meta: "data" },
// XXX: Need to ensure manually
task: TaskSchema.parse({
name: "my task",
slug: "task:" + taskSlug,
completed: false,
due_date: new Date(),
}),
};
}
},
);
router.post(
"/attachment",
class extends OpenAPIRoute {
static schema = {
tags: ["Tasks"],
summary: "Binary attachment",
responses: {
"200": {
description: "Binary attachment",
contentType: "application/pdf",
// See https://github.com/cloudflare/itty-router-openapi/issues/99
// schema: new Str({ format: "binary" }),
},
},
};
async handle() {
return new Response(new Blob(["ABC"]), {
headers: { "content-type": "application/pdf" },
});
}
},
);
という感じで、
- Inputはバリデーションされるけど
- Outputはバリデーションされないし、型もつかない
- 各ハンドラも型付けは自前
このあたりが仕方がないとはいえ、惜しいなって。
おまけ
せっかくitty-router
で軽量路線なのに、zod
にべったりなのも惜しい。
Support for Valibot types · Issue #96 · cloudflare/itty-router-openapi https://github.com/cloudflare/itty-router-openapi/issues/96
Oh…😢
Zodのサイズが気になるからあちこち放浪してるのに、本当にどこいってもZodばっかなので、一周回ってある日突然Zodが軽くなるよう念じてる今日この頃。
でもXtoOpenAPIの実装がいっぱい生まれるのも微妙なのは理解できるし、悩ましい。
こういう、雨後の筍な様相のバリデーションライブラリの互換性を保つやつがあるくらいやし。