🔥

Hono[炎]とZod OpenAPIでAPIドキュメントを書くのが楽しくなりそうです、本当に。

2025/02/09に公開

0. はじめに

最近、私はHonoというフレームワークの学習を始めました。その中で、Zod OpenAPI というライブラリを使えば、簡単にOpenAPIのドキュメントを生成できることを知り、その便利さに感動しました!

本記事では、その体験を共有しつつ、Zod OpenAPI を活用した効率的なAPIドキュメントの作成方法を紹介していきます。🚀

今回デモで作成したAPIドキュメント

また今回作成したコードはGithubにて参照可能です。

1. まあ作って学んでいこうや🔥

今回は以下のようなAPIのドキュメントをOpen APIで作成していきたいと思います💪
Node.js ver.でHonoのプロジェクトを作成しています。

Node.js以外にもCloudflare WorkersAWS Lambda用のプロジェクトも作成可能です。

・/users: ユーザー一覧を取得
・/users/{userId}: 任意のユーザー情報を取得

ディレクトリ構成は以下のようにしたいと思います。

ディレクトリ構成はこちら
.
├── src/
│   └── api-doc/
│       ├── handlers/
│       │   └── user.ts
│       ├── routes/
│       │   └── user.ts
│       └── schemas/
│           ├── paginaion.ts
│           └── user.ts
└── index.ts

1-1. 必要なnpmパッケージをインストールする&サーバー起動

まずは以下パッケージをインストールしてください。今回はpnpmを使用していますが、皆さんの環境に合わせてnpmやyarnに置き換えてください。

@hono/swagger-uiは「0. はじめに」で添付したドキュメントのようにAPIドキュメントを表示するために必要となります。

> pnpm i hono zod @hono/zod-openapi @hono/swagger-ui
> pnpm dev
// Server is running on http://localhost:3000

参考:zod-openapi

1-2. User & PaginationのSchemaを作成する

次にUserとPaginationのSchemaを作成していきます。
ユーザー一覧を取得する時に一気に数百件・数千件と取得するのはパフォーマンス的によろしくないので、page毎に分割して取得することが多いです。なのでPaginationのSchemaも作成しています。

/src/api-doc/schemas/user.ts
import { z } from "@hono/zod-openapi";

export const UserSchema = z
  .object({
    id: z.number().openapi({
      example: 1,
    }),
    name: z.string().openapi({
      example: "John Doe",
    }),
    age: z.number().openapi({
      example: 42,
    }),
  })
  .openapi("UserSchema");

export const User = UserSchema.openapi("User");
/src/api-doc/schemas/pagination.ts
import { z } from "@hono/zod-openapi";

export const PaginationSchema = z
  .object({
    page: z.number().openapi({
      example: 1,
    }),
    limit: z.number().openapi({
      example: 10,
    }),
  })
  .openapi("PaginationSchema");

export const Pagination = PaginationSchema.openapi("Pagination");

1-3. データを返却するハンドラーを作成する

次に実際にユーザー一覧やユーザー詳細のデータを返却するハンドラー部分を作成していきます。

/src/api-doc/handlers/user.ts
import { getUsersRoute, getUserRoute } from "@/api-doc/routes/user.js";
import type { User } from "@/api-doc/schemas/user.js";
import type { RouteHandler } from "@hono/zod-openapi";
import type { z } from "@hono/zod-openapi";

type UserList = z.infer<typeof User>;

export const getUsers: RouteHandler<typeof getUsersRoute, {}> = async (c) => {
  try {
    const users: UserList[] = [
      {
        id: 1,
        name: "John Doe",
        age: 42,
      },
      {
        id: 2,
        name: "Jane Doe",
        age: 30,
      },
    ];

    return c.json(users, 200);
  } catch (e) {
    console.error(e);
    return c.json({ message: "Internal Server Error", stackTrace: e }, 500);
  }
};

export const getUser: RouteHandler<typeof getUserRoute, {}> = async (c) => {
  try {
    const user = { id: 1, name: "John Doe", age: 42 };
    return c.json(user, 200);
  } catch (e) {
    console.error(e);
    return c.json({ message: "Internal Server Error", stackTrace: e }, 500);
  }
};

1-4. Userのルーティングを作成する

次にUserのルーティングを定義していきます。

/src/api-doc/routes/user.ts
import { createRoute } from "@hono/zod-openapi";
import { User } from "@/api-doc/schemas/user.js";
import { Error } from "@/api-doc/schemas/error.js";
import { z } from "@hono/zod-openapi";
import { OpenAPIHono } from "@hono/zod-openapi";
import { getUsers, getUser } from "@/api-doc/handlers/user.js";
import { Pagination } from "@/api-doc/schemas/pagination.js";

export const getUsersRoute = createRoute({
  method: "get",
  tags: ["User"],
  summary: "ユーザー一覧を取得する",
  operationId: "getUsers",
  path: "/",
  request: {
    query: Pagination,
  },
  responses: {
    200: {
      content: {
        "application/json": {
          schema: z.array(User),
        },
      },
      description: "Success",
    },
    500: {
      content: {
        "application/json": {
          schema: Error,
        },
      },
      description: "Internal Server Error",
    },
  },
});

export const getUserRoute = createRoute({
  method: "get",
  tags: ["User"],
  summary: "特定のユーザー情報を取得する",
  operationId: "getUser",
  path: "/{id}",
  responses: {
    200: {
      content: {
        "application/json": {
          schema: User,
        },
      },
      description: "Success",
    },
    500: {
      content: {
        "application/json": {
          schema: Error,
        },
      },
      description: "Internal Server Error",
    },
  },
});

export const userApi = new OpenAPIHono();

userApi.openapi(getUsersRoute, getUsers);
userApi.openapi(getUserRoute, getUser);

1-5. index.tsにrouteを登録

最後に/api-doc/index.tsに先ほど作成したUserのルーティングを登録します。

/src/api-doc/index.ts
import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi";
import { userApi } from "@/api-doc/routes/user.js";
import { taskApi } from "@/api-doc/routes/task.js";

export const api = new OpenAPIHono();

api
  .route("/users", userApi)
  .doc("/specification", {
    openapi: "3.0.0",
    info: {
      title: "API",
      version: "1.0.0",
    },
  })
  .get(
    "/",
    swaggerUI({
      url: "/api-doc/specification",
    })
  );

ここまでの作業を終えてhttp://localhost:3000/api-docにアクセスすると以下のようなドキュメントを表示することができます!

Open APIのドキュメントを作成する時にyml or jsonで書くことのが主流だったと思いますが、これをtsファイルで書ける体験が良いのでぜひ皆さん試してみてください!

参考文献

Discussion