21
9

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を試す。良いWEBフレームワークですね。

Last updated at Posted at 2024-05-16

最近人気の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

app/controllers/book.ts
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;

app/routes/api.tsx
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;

gamen_test1.gif

zod-validator

app/controllers/book.ts
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

app/repositories/book/list/get.ts
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",
      },
    ],
  });
};

app/controllers/book.ts
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;

image.png

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;

gamen_test1.gif

Basic認証

app/routes/api.tsx
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;

gamen_test1.gif

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に慣れている開発者向けのメモアプリとか、なんかいい感じのがすぐに作れそう。

app/routes/types.ts
export type Meta = {
  title: string;
};
app/routes/mdxs.tsx
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>
  );
}
app/routes/posts/test.mdx
---
title: Hello Hono
---

# Test Hono MDX!

this is sample mdx file

<div style="color: red;">color red</div>

image.png

3.ReactらしいHooksが用意されている

HonoXというよりHonoですが、useStateuseEffectuseCallbackなどReactで馴染みのあるフックが用意されている。リプレイスするときに苦にならなそう。

まとめ

以上Honoを試した所感でした。
RPC機能だけでもリプレイスするときに使いたいくらい素敵だと感じましたが、それ以外の機能もかなり充実していてとても良いWEBフレームワークだなと思いました。Expressで構築した部分もすぐにリプレイスできそう。
また、公式でも言うようにとにかく早い。開発体験が良いのも魅力的。
近い将来(もうなってるかもですが)「Honoはいいぞ」と言われてそうな予感。これからはキャッチアップも兼ねてより積極的にHonoを使っていきたいと思います。

おまけ

開発者の1人@usualomaさん、@sawaratsuki1004さんによるプロモーション動画を見つけました。かわいらしいデザインが良いですね。

21
9
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
21
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?