3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Honoがすごい】APIドキュメントを自動生成&単体テスト

Last updated at Posted at 2024-11-19

はじめに

どうも、web系からシステム開発にジョブチェンしたばかりの@kaiparuです。
今回は名前のとおり激アツな Hono というフレームワークを使って爆速で開発する方法について書いていきます。

Honoとは

Honoとは高速、軽量かつWeb標準APIのみを使用したWebアプリケーションフレームワークです。開発者は日本人で、2022年にプロジェクトが始動したばかりのフレッシュなフレームワークです。
詳しくは公式ドキュメントや開発者のブログをお読みください。

Hono公式ドキュメント

開発者のブログ

環境

  • bun: v1.1.4
  • hono: v4.4.8
  • @hono/zod-openapi: v0.14.5
  • @hono/swagger-ui: v0.3.0

プロジェクトの作成

今回はBunを使ってサクッとHonoのプロジェクトを作っていきます。
(基礎的な部分は大丈夫だから本題に進みたい、という方はドキュメントの自動生成まで飛ばしてください。)

作成手順の詳細は公式ドキュメントを見てください。

Bunのインストール

すでにbunが入っている人は飛ばしてOKです。

curl -fsSL https://bun.sh/install | bash

Honoプロジェクトの作成

いくつか質問されるので好みのものを答えてください。テンプレートはbunを選択します。
私の回答を参考に載せておきます。

# hono-app は任意の名称
bun create hono hono-app

# Which template do you want to use?
# > bun
# Do you want to install project dependencies?
# > Y
# Which package manager do you want to use?
# > bun

これだけで、Honoがあらかじめインストールされた状態でプロジェクトを開始できます。作成されたプロジェクトのpackage.jsonは下記のようになっています。

package.json
{
  "name": "hono-app",
  "scripts": {
    "dev": "bun run --hot src/index.ts"
  },
  "dependencies": {
    "hono": "^4.4.8"
  },
  "devDependencies": {
    "@types/bun": "latest"
  }
}

さらにsrc/index.tsが用意されていて、ルートへアクセスすると'Hello Hono!'と表示されるAPIが定義されています。

src/index.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

export default app

起動してみる

Bunのプロジェクトを立ち上げてみます。

# プロジェクトのディレクトリへ移動
cd hono-app

# run
bun run dev

# $ bun run --hot src/index.ts
# Started server http://localhost:3000

http://localhost:3000にアクセスして下記のように表示されればOKです。
スクリーンショット 2024-06-27 9.54.33.png

ドキュメントの自動生成

今回、Honoの提供する3rd-party Middlewareの@hono/zod-openapi@hono/swagger-uiを使ってドキュメントを自動生成します。

Zod OpenAPI

Swagger UI

準備

まずこれらを使うためにインストールします。

bun install @hono/zod-openapi
bun install @hono/swagger-ui

例として今回は/user/:idへGETリクエストを投げると、「id・年齢・名前・電話番号」のjsonデータが返ってくるAPIを定義します。例なので年齢・名前・電話番号は固定値としますが、実際のシステム等ではDBに問い合わせて情報を取ってきてると思ってください。

src/index.ts
import { Hono } from "hono";

const app = new Hono();

app.get("/user/:id", (c) => {
  const id = c.req.param("id");
  return c.json({
    id,
    age: 25,
    name: "Kaeya Alberich",
    tel: "0123-456-789",
  });
});

export default app;

ルーティング

わざわざ分ける必要はないですが、APIが複数ある場合はファイル内のコードが長くなり、視認性が悪くなってしまいます。実用性を考えて、ルーティング用ファイルとAPI処理用ファイルで分けます。
さらに巨大なコードになる場合は、必要に応じてスキーマ定義も別ファイルに切り出すなどして工夫しましょう。

srcディレクトリ配下にindex.route.tsを作成します。

src/index.route.ts
import { z, createRoute } from "@hono/zod-openapi";

// -------- Schemas --------
const ParamsSchema = z.object({
  id: z
    .string()
    .min(1)
    .max(3)
    .openapi({
      param: {
        name: "id",
        in: "path",
      },
      example: "100",
    }),
});

const UserOutputSchema = z
  .object({
    id: z.string().min(1).max(3).openapi({
      example: "100",
    }),
    age: z.number().min(1).max(3).openapi({
      example: 25,
    }),
    name: z.string().openapi({
      example: "Kaeya Alberich",
    }),
    tel: z
      .string()
      .max(13)
      .regex(/^\d+-\d+-\d+$/)
      .optional()
      .openapi({
        example: "0123-456-789",
      }),
  })
  .openapi("User");

// -------- GET /users/{id} --------
export const getUserIdRoute = createRoute({
  method: "get" as const,
  path: "/users/{id}",
  summary: "get user info",
  description: "idでユーザーの情報を取得する",
  request: {
    params: ParamsSchema,
  },
  responses: {
    200: {
      description: "success",
      content: {
        "application/json": {
          schema: UserOutputSchema,
        },
      },
    },
    400: {
      description: "Invalid Id",
      content: {
        "application/json": {
          schema: z.object({
            message: z.string().openapi({
              example: "Invalid Id",
            }),
          }),
        },
      },
    },
    404: {
      description: "Not Found",
      content: {
        "application/json": {
          schema: z.object({
            message: z.string().openapi({
              example: "User not found",
            }),
          }),
        },
      },
    },
  },
});
export type GetUserIdRouteResponse200 = z.infer<
  (typeof getUserIdRoute.responses)["200"]["content"]["application/json"]["schema"]
>;
export type GetUserIdRouteResponse400 = z.infer<
  (typeof getUserIdRoute.responses)["400"]["content"]["application/json"]["schema"]
>;
export type GetUserIdRouteResponse404 = z.infer<
  (typeof getUserIdRoute.responses)["404"]["content"]["application/json"]["schema"]
>;

各所に書いてある.openapi("User") z.object() z.string() .min() .optional()などで、スキーマ定義とバリデーション定義を一緒にできてしまうのがzodの便利なところです。

API処理

次にindex.tsを下記のように修正します。

src/index.ts
import { OpenAPIHono } from "@hono/zod-openapi";
import { swaggerUI } from "@hono/swagger-ui";
import { getUserIdRoute } from "./index.route";

const app = new OpenAPIHono();

// -------- GET /users/{id} --------
app.openapi(getUserIdRoute, (c) => {
  const { id } = c.req.valid("param");
  if (id.length > 3) {
    return c.json({ message: "Invalid Id" }, 400);
  } else if (id === "999") {
    return c.json({ message: "Not Found" }, 404);
  } else {
    return c.json(
      {
        id,
        age: 25,
        name: "Kaeya Alberich",
        tel: "0123-456-789",
      },
      200
    );
  }
});

// -------- ドキュメント生成 --------
app.doc("/docs/json", {
  openapi: "3.0.0",
  info: {
    version: "1.0.0",
    title: "APIドキュメント",
    description: `## 概要
APIを爆速でドキュメント化するためのサンプルプロジェクトです。
`,
  },
});
app.get("/docs/UI", swaggerUI({ url: "/docs/json" }));

export default app;

生成されたドキュメントを確認してみる

bun run devで起動した状態で、http://localhost:3000/docs/jsonにアクセスしてみます。jsonデータが返ってきていればOKです。

次にhttp://localhost:3000/docs/UIにアクセスしてみます。SwaggerUIのドキュメントが表示されればOKです。
swagger-ui.png

単体テスト

jestを使っている方が多いと思うので、今回はjestを使います。
Honoのドキュメントでもjestを使ったテストを紹介しています。

パッケージのインストール

TypeScriptでjestを使うには下記の3つのパッケージをインストールします。

bun add jest ts-jest @types/jest --dev

jest.config.jsを作成

ルートディレクトリにjest.config.jsを作成します。

jest.config.js
/** @type {import("ts-jest/dist/types").InitialOptionsTsJest} */
module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
};

package.jsonにスクリプトを追加

package.json"test": "jest --detectOpenHandles"を追記します。

package.json
{
  "name": "hono-app",
  "scripts": {
    "dev": "bun run --hot src/index.ts",
    "test": "jest --detectOpenHandles"
  },
  "dependencies": {
    "@hono/swagger-ui": "^0.3.0",
    "@hono/zod-openapi": "^0.14.5",
    "hono": "^4.4.8"
  },
  "devDependencies": {
    "@types/bun": "latest",
    "@types/jest": "^29.5.12",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.5"
  }
}

テストファイルを作成

srcディレクトリ配下にindex.test.tsを作成します。
今回は「正常に取得できた場合」「不正なidだった場合」「存在しないidだった場合」の3パターンをテストします。

src/index.test.ts
import { describe, test, expect } from "@jest/globals";
import app from "./index";
import {
  GetUserIdRouteResponse200,
  GetUserIdRouteResponse400,
  GetUserIdRouteResponse404,
} from "./index.route";

// -------- GET /users/{id} --------
describe("GET /users/{id}", () => {
  test("[success] idを指定してユーザーの情報を取得する", async () => {
    const userId = "100";
    const res = await app.request(`/users/${userId}`, {
      method: "GET",
    });
    expect(res.status).toBe(200);
    const body = (await res.json()) as GetUserIdRouteResponse200;
    expect(body).toEqual({
      id: userId,
      age: 25,
      name: "Kaeya Alberich",
      tel: "0123-456-789",
    });
  });
  test("[error] idが不正", async () => {
    const invalidUserId = "1000";
    const res = await app.request(`/users/${invalidUserId}`, {
      method: "GET",
    });
    expect(res.status).toBe(400);
  });
  test("[error] idが存在しない", async () => {
    const noExistUserId = "999";
    const res = await app.request(`/users/${noExistUserId}`, {
      method: "GET",
    });
    expect(res.status).toBe(404);
    const body = (await res.json()) as GetUserIdRouteResponse404;
    expect(body).toEqual({
      message: "Not Found",
    });
  });
});

テストの実施

下記のコマンドでテストを実行してみます。

bun run test ./src/index.test.ts

下記のようにテストが通ってくれればOKです。
test.png

まとめ

いかがだったでしょうか。

  • APIのルーティングや定義を書きながら
  • ドキュメントも自動生成できて
  • テストも簡単に書ける

を実感してもらえたかなと思います。
私自身もこれからもっとHonoを使いこなしていきたいですね〜

今回の記事で作成したサンプルコードを貼っておきますので、ご活用ください。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?