はじめに
どうも、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
は下記のようになっています。
{
"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が定義されています。
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です。
ドキュメントの自動生成
今回、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に問い合わせて情報を取ってきてると思ってください。
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
を作成します。
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
を下記のように修正します。
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です。
単体テスト
jestを使っている方が多いと思うので、今回はjestを使います。
Honoのドキュメントでもjestを使ったテストを紹介しています。
パッケージのインストール
TypeScript
でjestを使うには下記の3つのパッケージをインストールします。
bun add jest ts-jest @types/jest --dev
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"
を追記します。
{
"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パターンをテストします。
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
まとめ
いかがだったでしょうか。
- APIのルーティングや定義を書きながら
- ドキュメントも自動生成できて
- テストも簡単に書ける
を実感してもらえたかなと思います。
私自身もこれからもっとHonoを使いこなしていきたいですね〜
今回の記事で作成したサンプルコードを貼っておきますので、ご活用ください。