最近人気のWebフレームワークHono
2024年2月に Hono v4 がリリースされ、同時に メタフレームワーク HonoX が公開されました。気になっていたのでそれぞれ試してみました。
「結構良さげ」と思ったことをつらつらと書いています。
検証環境
"bun": "^1.1.8"
"hono": "^4.3.6"
"honox": "^0.1.17"
Hono v4.0.0
1.RPC機能
バックエンド・フロントエンド両方をTSで構築する場合、問題は「型の共有」。
HonoではRPC機能を提供しており(いわゆるtRPC
でできることをHonoではREST APIを書くだけで実現できる)、HonoXでももちろん利用可能。
良いなと思うのが「スキーマ駆動」で開発できるところ。Honoは提供するサードーパーティライブラリが豊富。zodでバリーデーションしたり、OpenAPI UIまで生成できる。
RPC
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
const schema = z.object({
name: z.string(),
});
export const bookController = new Hono()
.get("/list", (c) =>
c.json({
result: [
{
id: 1,
title: "テストbook1",
description: "説明1",
},
{
id: 2,
title: "テストbook2",
description: "説明2",
},
{
id: 3,
title: "テストbook3",
description: "説明3",
},
],
})
)
.post("/", (c) => c.json({ result: "create a book" }, 201));
export type BookControllerType = typeof bookController;
import { Hono } from "hono";
import { logger } from "hono/logger";
import { bookController } from "../controllers/book";
const app = new Hono();
app.use("*", logger());
app.route("/book", bookController);
export default app;
zod-validator
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
const schema = z.object({
name: z.string(),
});
export const bookController = new Hono()
.
.
.
.get("/:id", zValidator("query", schema), (c) => {
const { name } = c.req.valid("query");
return c.json({ result: `get ${c.req.param("id")}. name is ${name}` });
})
export type BookControllerType = typeof bookController;
curl http://localhost:5173/api/book/devPROF0F5?id=hoge
{
"success": false,
"error": {
"issues": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"name"
],
"message": "Required"
}
],
"name": "ZodError"
}
}
OpenAPI UI
import { RouteHandler, createRoute, z } from "@hono/zod-openapi";
export const routeDifinision = createRoute({
path: "/list",
method: "get",
description: "本のリストを返す",
responses: {
200: {
description: "OK",
content: {
"application/json": {
schema: z.object({
result: z.array(
z.object({
id: z.string().openapi({
example: "devFOGG0F9dvO=",
description: "Book ID",
}),
title: z.string().openapi({
example: "テストBook1",
description: "本のタイトル",
}),
description: z.string().openapi({
example: "テスト説明1",
description: "本の説明",
}),
})
),
}),
},
},
},
},
});
export const resolver: RouteHandler<typeof routeDifinision> = (context) => {
return context.json({
result: [
{
id: "fvosovmmvosmo",
title: "テストbook1",
description: "説明1",
},
{
id: "mdsmvosdvmo",
title: "テストbook2",
description: "説明2",
},
{
id: "ndosnvovnson",
title: "テストbook3",
description: "説明3",
},
],
});
};
import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi";
import {
resolver as bookListGetResolver,
routeDifinision as bookListGetRouteDifinision,
} from "../repositories/book/list/get";
import {
resolver as bookPostResolver,
routeDifinision as bookPostRouteDifinition,
} from "../repositories/book/post";
const bookController = new OpenAPIHono()
.openapi(bookListGetRouteDifinision, bookListGetResolver)
.openapi(bookPostRouteDifinition, bookPostResolver);
bookController.get(
"/ui",
swaggerUI({
url: "/api/book",
})
);
bookController.doc("/", {
openapi: "3.1.0",
info: {
title: "Book API",
version: "v1",
},
});
export default bookController;
2.Helper/Middlewareが豊富
JWTなどよくある認証実装もHelperやMiddlewareを使えばスッキリする。
JWT Token
import { Hono } from "hono";
import { deleteCookie, setCookie } from "hono/cookie";
import { jwt, sign } from "hono/jwt";
const payload = {
sub: "test_user",
role: "admin",
exp: Math.floor(Date.now() / 1000) + 60 * 5, // 有効期限 5分
};
const secret = "ENV_JWT_SECRET_KEY";
const token = await sign(payload, secret);
const app = new Hono();
app.get("/login", (c) => {
setCookie(c, "hono-app", token);
return c.text("success login.");
});
app.get("/logout", (c) => {
deleteCookie(c, "hono-app");
return c.text("success logout.");
});
app.use("/admin/*", (c, next) => {
const jwtMiddleware = jwt({
cookie: "hono-app",
secret,
});
return jwtMiddleware(c, next);
});
app.get("/admin/page", (c) => {
return c.text("You are authorized");
});
export default app;
Basic認証
import { Hono } from "hono";
import { basicAuth } from "hono/basic-auth";
import { logger } from "hono/logger";
import bookController from "../controllers/book";
const app = new Hono();
app.use("*", logger());
app.use(
"/*",
basicAuth({
username: "hono",
password: "password",
})
);
app.get("/auth/page", (c) => {
return c.text("You are authorized");
});
app.route("/book", bookController);
export default app;
3.SSGができる
Cloudflare Pagesにデプロイする選択肢がまた1つ増えた。
HonoX
1.File-based routing
HonoXでは、Next.jsのようにファイルを置いた場所によってルーティングができる。
app/routes/index.tsx => /
app/routes/about/hoge.tsx => /about/hoge
app/routes/articles/[:id].tsx => /articles/:id
また、Islandsアーキテクチャを採用しているので、通常はServer Component として動作するが、app/islands
以下に置いてルートファイルでimportするとClient Components
とみなし、部分的なコンポーネント動作を可能にしている。
2.MDXが使える
依存ライブラリをインストールしてvite.config.ts
を設定するだけでMDXが使える。
mdに慣れている開発者向けのメモアプリとか、なんかいい感じのがすぐに作れそう。
export type Meta = {
title: string;
};
import { Meta } from "./types";
export default function Top() {
const posts = import.meta.glob<{ frontmatter: Meta }>("./posts/*.mdx", {
eager: true,
});
console.log({ posts });
return (
<div>
<h2>Posts</h2>
<ul class="article-list">
{Object.entries(posts).map(([id, module]) => {
if (module.frontmatter) {
return (
<li>
<a href={`${id.replace(/\.mdx$/, "")}`}>
{module.frontmatter.title}
</a>
</li>
);
}
})}
</ul>
</div>
);
}
---
title: Hello Hono
---
# Test Hono MDX!
this is sample mdx file
<div style="color: red;">color red</div>
3.ReactらしいHooksが用意されている
HonoXというよりHonoですが、useState
、useEffect
、useCallback
などReactで馴染みのあるフックが用意されている。リプレイスするときに苦にならなそう。
まとめ
以上Honoを試した所感でした。
RPC機能だけでもリプレイスするときに使いたいくらい素敵だと感じましたが、それ以外の機能もかなり充実していてとても良いWEBフレームワークだなと思いました。Expressで構築した部分もすぐにリプレイスできそう。
また、公式でも言うようにとにかく早い。開発体験が良いのも魅力的。
近い将来(もうなってるかもですが)「Honoはいいぞ」と言われてそうな予感。これからはキャッチアップも兼ねてより積極的にHonoを使っていきたいと思います。
おまけ
開発者の1人@usualomaさん、@sawaratsuki1004さんによるプロモーション動画を見つけました。かわいらしいデザインが良いですね。