社内プロダクトにHono🔥を採用した話

この記事は株式会社エス・エム・エス Advent Calendar 2024の12月12日の記事です。

エス・エム・エスで人材紹介の社内基盤開発をしている熊谷です。 今回はそんな社内システムの1つで、Honoを使って開発している事例をご紹介します。

なぜ私たちはHonoを採用したのか?

人材紹介事業では、CP(キャリアパートナー)と呼ばれる、従事者様の転職のお手伝いをサポートする人員がいます。 従事者様とCPとのコミュニケーションは一般的に電話やメールなどで行われますが、LINEもよくコミュニケーションによく使われる手段の1つです。

従事者様がエス・エム・エスが展開している公式LINEに登録すると、公式LINE上で従事者様専任に割り振られたCPと従事者様との1to1のコミュニケーションが可能になります。 これらを実現するために、LINE Messaging APIを使った中間のWebサーバを社内で管理しています。

元々これらの仕組みは、EC2 + PHPという組み合わせで作られていたのですが、

  • コミュニケーション量が増えるに伴って費用も増えてきた
  • 処理としては簡易で、EC2で稼働させる必要性がない
  • 時間帯や曜日によって負荷に増減があるため、スケールしやすい基盤が向いている

という事情から、Lambda + Node.jsで書き換える企画がスタートしました。

このWebサーバの要件をざっくり伝えると

  • シンプルだけど10ページ分のWebページ(フォーム画面)が必要
  • Cookieを使う(サーバサイド処理が必要)

というもので、色々なフレームワークを比較検討した結果、Honoが良さげだぞという結論に至りました。 決定打としては「jsxがテンプレートとして使えること」「Ryan Dahl氏がDeno FestでHonoを推してたこと」の2つでした。

採用してみてどうだったか?

非常に良い!!!

というのが感想です、私たちが開発していて実際に良かった点を列挙していきます。

テストが書きやすい

Web Standard APIのRequest/Responseを利用しているからこそ、リクエスト/レスポンスのテストの書きやすさが素晴らしいです

 /**
 * index.ts
 */
import { Hono } from "hono";
const app = new Hono();

app.get("/hello/:name", (c) => {
  const name = c.req.param("name");
  return c.text(`Hello, ${name}`);
});

export default app;
/**
 * indx.test.ts
 */
import app from "./index";

test("GET /hello/:name すると、'Hello, ${name}' が返ってくる", async () => {
  const path = "kumagai";
  // app.request()で定義済みルートへアクセスでき、Responseオブジェクトが返却される
  const res = await app.request(`/hello${path}`);
  expect(res.status).toBe(200);
  expect(res.text()).toBe(`Hello, ${path}`);
});

zod + reuqest parameter validation

zodを使ったランタイムバリデーションがリクエストパラメータ等の検証に簡単に適合できるのが良いです

const schema = z.object({
  id: z.string().uuid(),
});
app.post("/entry", zValidator("form", schema), (c) => {
  c.status(201);
  return c.text("Created");
});
test("POST /entry は、FormDataでid = uuidのみ受け付ける", async () => {
  const body = new FormData();
  // UUID以外はBadRequest
  body.append("id", "abc12345");
  const res1 = await app.request("/entry", { method: "POST", body });
  expect(res1.status).toBe(400);

  // UUIDは201
  body.append("id", "fc56abe1-d352-691e-bd8e-13102bf17549");
  const res2 = await app.request("/entry", { method: "POST", body });
  expect(res2.status).toBe(201);
  expect(await res2.text()).toBe("Created");
});

jsxテンプレート

jsxで直感的にフロントエンドが書けるのが素晴らしすぎる

/**
 * todo.tsx
 */
import { Hono } from "hono";
import { jsxRenderer } from "hono/jsx-renderer";
import type { FC } from "hono/jsx";

const app = new Hono();

app.use(
  "/todo/*",
  jsxRenderer(({ children }) => {
    return (
      <html lang="ja">
        <title>My ToDo</title>
        <body>{children}</body>
      </html>
    );
  }),
);

const ToDoList: FC<{ items: string[] }> = ({ items }) => {
  return (
    <ul>
      {items.map((item) => (
        <li>{item}</li>
      ))}
    </ul>
  );
};

app.get("/", (c) => {
  return c.render(
    <>
      <h1>今日のToDoは?</h1>
      <ToDoList items={["掃除", "昼寝", "歯磨き"]} />
    </>,
  );
});

export default app;

最後に

私たちがHonoを使っていて、とても感じるのは

「Honoって楽しい!!!」

です。チームとして迷わず開発できている点や、開発生産性も高く、何より使っていて気持ちが良い というのが大きいと思います。

社内のプロダクトで使い始めたHonoですが、個人開発でも何かとHonoをベースに開発する機会が増えました。個人的には Bun + Honoで作ることが多く、TypeScriptやテストの取り回しも楽だし、Cloudflare Workerなどどこでも動くのは本当に魅力だと思っています。

この記事が、皆さんのHono採用きっかけの一助になればと思っています。