🔥

Hono入門 - @hono/openapi-zodでTodo API開発 -

2024/11/30に公開

はじめに

普段はフロントエンドを中心に触れていますが、最近バックエンドにも興味があり、話題のHonoに入門してみました。とりあえずTodo APIの開発をやってみました!

環境構築

公式ドキュメント通りです。テンプレートはcloudflare-workersを選択しました。

yarn create hono my-app

https://hono.dev/docs/getting-started/cloudflare-workers

Module Worker mode

ドキュメントでも言及されていますが、Cloudflare WorkersにはService Worker mode と Module Worker mode というものがあります。今回は登場しませんがBindingsというCloudflareのミドルウェア(KV, R2, D1など)にアクセスするためのものが存在します。Module Worker modeではこのBindingsをローカルスコープで扱えるなどのメリットがあるため基本的には Module Worker modeで使用するのが良いでしょう。

src/index.ts
// Module Worker
export default app

スキーマ定義

パッケージ追加

まずは以下を追加します。
Swaggerでドキュメント作成もしたいので@hono/swagger-uiも追加します。

yarn add @hono/zod-openapi @hono/swagger-ui

次に@hono/zod-openapiを使ってスキーマを定義していきます。

src/models/todos.ts
import { z } from "@hono/zod-openapi";

export const TodoSchema = z
  .object({
    id: z.number().openapi({ example: 1 }),
    title: z.string().openapi({ example: "Learning Hono" }),
    completed: z.boolean().openapi({ example: false }),
  })
  .openapi("TodoSchema");

export const TodoListSchema = z.array(TodoSchema).openapi("TodoListSchema");

// GET, PUT, DELETE用のリクエストモデル
export const TodoParamSchema = z.object({
  id: z.string().openapi({ example: "1" }),
});

// Post用のリクエストモデル
export const CreateTodoSchema = z
  .object({
    title: z.string().openapi({ example: "Learning Hono" }),
  })
  .openapi("CreateTodoSchema");

エラー用のスキーマも定義しました。

src/models/error.ts
import { z } from "@hono/zod-openapi";

export const MessageSchema = z.object({
  code: z.number().openapi({
    example: 400,
  }),
  message: z.string().openapi({
    example: "Bad Request",
  }),
});

router定義

各エンドポイントの実装をしていきます。(GETとPOST以外は割愛)
@hono/zod-openapicreateRouteを使用してルート定義をします。
あらかじめファイルは分割しています。分割せずにsrc/index.tsにルート定義している記事が多かったですが、分割した側をNested routeとして追加してあげるといいです。

src/routes/todo.ts
import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
import { TodoSchema, CreateTodoSchema } from "../models/todos";
import { MessageSchema } from "../models/error";

export const app = new OpenAPIHono();

const todoList = [
  {
    id: "1",
    title: "Learning Hono",
    completed: false,
  },
  {
    id: "2",
    title: "Implement Todo API",
    completed: true,
  },
  {
    id: "3",
    title: "Write documentation",
    completed: false,
  },
];

// GET Todo
const getTodoRoute = createRoute({
  method: "get",
  path: "/{id}",
  request: {
    params: TodoParamSchema,
  },
  responses: {
    200: {
      content: {
        "application/json": {
          schema: TodoSchema,
        },
      },
      description: "Get Todo",
    },
    400: {
      description: "Bad Request",
      content: {
        "application/json": {
          schema: MessageSchema,
        },
      },
    },
    404: {
      content: {
        "application/json": {
          schema: MessageSchema,
        },
      },
      description: "Not Found",
    },
  },
  tags: ["todo"],
});

app.openapi(
  getTodoRoute,
  (c) => {
    const { id } = c.req.valid("param");
    const todo = todoList.find((todo) => todo.id === id);

    if (!todo) return c.json({ code: 404, message: "Not Found" }, 404);

    return c.json(todo, 200);
  },
  (result, c) => {
    if (!result.success) {
      return c.json(
        {
          code: 400,
          message: "Validation Error",
        },
        400
      );
    }
  }
);

// POST Todo
const createTodoRoute = createRoute({
  method: "post",
  path: "/",
  request: {
    body: {
      content: {
        "application/json": {
          schema: CreateTodoSchema,
        },
      },
    },
  },
  responses: {
    200: {
      content: {
        "application/json": {
          schema: TodoSchema,
        },
      },
      description: "Create Todo",
    },
    400: {
      description: "Bad Request",
      content: {
        "application/json": {
          schema: MessageSchema,
        },
      },
    },
  },
  tags: ["todo"],
});

app.openapi(
  createTodoRoute,
  async (c) => {
    const { title } = await c.req.json();

    const newTodo = {
      id: String(todoList.length + 1),
      title,
      completed: false,
    };

    todoList.push(newTodo);
    return c.json(newTodo, 200);
  },
  (result, c) => {
    if (!result.success) {
      return c.json(
        {
          code: 400,
          message: "Validation Error",
        },
        400
      );
    }
  }
);

最後に

src/index.ts
import { OpenAPIHono } from "@hono/zod-openapi";
import { app as todoRoute } from "./routes/todo";

const app = new OpenAPIHono();

app.get("/", (c) => {
  return c.json({
    ok: true,
    message: "Hello Hono",
  });
});
// Nested routeとしてtodoRouteを追加
app.route("/todo", todoRoute);

export default app;

Swagger UI

Swaggerドキュメント作成のために以下を追加します。

src/index.ts
import { OpenAPIHono } from "@hono/zod-openapi";
+ import { swaggerUI } from "@hono/swagger-ui";
import { app as todoRoute } from "./routes/todo";

const app = new OpenAPIHono();

app.get("/", (c) => {
  return c.json({
    ok: true,
    message: "Hello Hono",
  });
});

app.route("/todo", todoRoute);

+ app.doc("/doc", {
+  openapi: "3.0.0",
+ info: {
+    version: "1.0.0",
+    title: "My TODO API with Hono",
+  },
+ });

+ app.get("/ui", swaggerUI({ url: "/doc" }));

export default app;

いい感じ!

終わりに

@hono/openapi-zod を使い簡単にスキーマを定義しながらrouerの実装もできました。
まだまだ入門したてなのでこれからどんどん使っていこうと思います!
次回あればDBとの連携もやっていこうと思いますー。

Discussion