【最強】Honoフル活用事例2024年
Hono アドベントカレンダー 2024 初日担当の おりばー です。
本記事では、11 月にリリースした漫画プラットフォーム「comilio」の開発事例をもとに、とにかく Hono が最強だということをつらつらと書いていく記事となります。
個人的 2024 年ベストオブ優勝フレームワークは Hono 一択です。Hono が無ければ、おそらくプロダクトを今もリリースできていなかったと言っても過言ではありません。
ぜひこの記事を参考にして、0 -> 1 を立ち上げる際は Hono を積極的に採用してもらえればと思います。
また、Hono という最高のプロダクトを生み出してくれた @yusukebe さんには全身全霊を持って感謝します。
「comilio」のインフラ構成
まず、今回の実例である漫画プラットフォーム「comilio」の構成を紹介します。
「comilio」では TypeScript で書かれたサーバーはほぼ全てに Hono を利用しています。作業分担の都合や技術難易度、エコシステムの都合で一部 Go など別の言語のサーバーやバッチが混ざっているのですが、基本は Hono で完結させるように心がけています。
Hono の詳細な使い方は本記事では省略させていただきますが、実装する上で特に感動的だった点をいくつかご紹介していきます。
また、今回のサンプルコードは以下にまとめてありますので、ご参考ください。
強力な RPC により、堅牢な開発が可能となる
Hono を利用することで、強力な型支援を受けることができます。
基本的に TypeScript プロジェクト上から Hono で実装されたサーバーにアクセスする際は、 hono/client
を利用するのが良いでしょう。例えば、簡易的な実装は以下となります。
import { Hono } from "hono";
const app = new Hono().get("/hello", (c) => c.text("Hono!"));
export type AppType = typeof app;
export default app;
import { AppType } from "./server";
import { hc } from "hono/client";
const client = hc<AppType>("http://localhost:8787/");
const res = await client.hello.$get();
if (res.ok) {
const text = await res.text();
console.log(text);
}
上記のコードで、例えば server.ts
の "/hello"
が "/hello2"
に変わった時、 client.ts
側の client.hello.$get()
は存在しなくなるので、Build 時にエラーとさせることが容易となります。
import { Hono } from "hono";
// hello2に変更
const app = new Hono().get("/hello2", (c) => c.text("Hono!"));
export type AppType = typeof app;
export default app;
import { AppType } from "./server";
import { hc } from "hono/client";
const client = hc<AppType>("http://localhost:8787/");
// hello -> hello2に変わってるので、ここでBuildエラーになる
const res = await client.hello.$get();
if (res.ok) {
const text = await res.text();
console.log(text);
}
また、レスポンスデータの型も自動的に生成されるため、レスポンスデータのフィールドが変わった場合、クライアント側の Build を落とすことも容易となります。
import { Hono } from "hono";
// user -> nameに変更したとする
const app = new Hono().get("/user", (c) => c.json( { name: "oliver" }));
export type AppType = typeof app;
export default app;
import { AppType } from "./server";
import { hc } from "hono/client";
const client = hc<AppType>("http://localhost:8787/");
const res = await client.user.$get();
if (res.ok) {
const json = await res.json();
// user -> nameに変わってるので、ここでBuildエラーになる
console.log(json.name);
}
この RPC 機能が pnpm のワークスペース との組み合わせに非常にマッチしており、サーバーとクライアント側でパッケージを分けておき、サーバー側の実装変更の影響でもしっかりとクライアント側で Build エラーとなってくれるため、安心して開発することが可能となります。
Hono + Drizzle + Zod が最強すぎる
プロジェクトが大規模になってきた際、どうしても出てくる悩みがバリデーションをどう行うかだと思います。
個人的にバリデーションの記述は TypeScript 上だけで完結させられることが理想であり、その前提だと Drizzle が公式で drizzle-zod
を提供しているので、これと Hono を組み合わせることで、非常に開発体験が良くなります。
特にバリデーションを TypeScript 上だけで完結させられることは、0 -> 1 開発において非常に重要なポイントだと考えてます。理由としては、ホットリロードでもすぐに反映される点や、バリデーションの値を TypeScript 上に定義できるようになるからです。
例えば、ユーザー名の最大文字数を 64 文字にしたとして、フロント側の Input でもその値を利用したいとなったとき、バリデーションの値を TypeScript 上の 1 つの場所に定義できることは、値変更時などの対応が簡単となるからです。
Next.js のバックエンド API として利用できる
「comilio」を開発する上で、非常に役に立ったのがこの機能です。
「comilio」は App Router で開発されているのですが、Server Actions 上から Hono のサーバーへリクエストする方針となってます。例えば、以下のようなコードになります。
"use server";
import { client } from "./client";
export async function fetchComic({ comicId }:{ comicId: string; }) {
const res = await client().public.comic[":comicId"].$get({
param: { comicId },
});
if (!res.ok) {
return notFound();
}
const json = await res.json();
if (json === null) {
return notFound();
}
return json;
}
「comilio」では基本的に全ての Server Actions でこのようなコードになっており、シンプルなコードになっています。このようにしている理由は、開発環境では Next.js の起動だけで開発したいが、本番環境ではバックエンドを分離したい(というケースが出てくる可能性がある)からです。
Hono のすごい点は、そのポータビリティです。「comilio」の開発環境では Next.js を立ち上げた際、バックエンドのコードは Next.js 上の API Routes にマウントされるようになっています。これにより開発環境は Next.js の起動だけで済み、本番環境ではバックエンドを別のところに配置する。ということが可能となります。
要するに、Next.js を Vercel にデプロイしたいけれど、コストの都合や、Database の IP 制限などの要件に対応するためにバックエンドだけ CloudRun など別の場所にデプロイすることがめちゃくちゃ簡単にできる。ということです。
実際のコードは以下のようなイメージとなります。
import { app as backend } from "@hono-advent-calendar-2024/backend";
import { Hono } from "hono";
import { handle } from "hono/vercel";
const handleDevOnly = (...args: Parameters<ReturnType<typeof handle>>) => {
if (process.env.NODE_ENV === "development") {
const app = new Hono().basePath("/api").route("/", backend);
return handle(app)(...args);
}
return new Response(null, { status: 404 });
};
export const runtime = "nodejs";
export const GET = handleDevOnly;
export const POST = handleDevOnly;
export const PUT = handleDevOnly;
export const PATCH = handleDevOnly;
export const DELETE = handleDevOnly;
import { hc } from "hono/client";
import type { AppType } from "@hono-advent-calendar-2024/backend";
import { getBaseURL } from "@/app/lib/baseUrl";
// getBaseURLでVercelとCloudRunを切り替える
export const client = hc<AppType>(`${getBaseURL()}/api`);
どのサービスも必要なインフラ構成は文脈によって変わってきます。その変数はローカル開発や本番環境であったり、コストやセキュリティ、パフォーマンスなど多岐にわたると思います。そして、そのインフラ構成の変更にコードを追従させなければいけなくなった際、Hono は最小の変更で済ませられるフレームワークと言えるでしょう。まさに 0->1 開発の救世主と言えます。
おわりに
まだまだ Hono について語りたいことは無限にあるのですが、また今度の機会に取っておきます。ぜひみなさんも Hono を使って最強の開発を体験しよう!
Discussion