Hono + Inertia + React のフォーム&CRUD実装パターン
Drizzle ORM・Zod・Inertia useForm を組み合わせる【2026年版】
Hono + Inertia + React で業務アプリの主役である CRUD・フォームをどう書くか。Drizzle ORM での DB 接続、Zod による型と入力検証の一気通貫、Inertia の useForm を使ったエラー表示・リダイレクト・フラッシュメッセージまで、実装パターンを実務目線で整理します。
ゴール — 業務アプリの主役を1スタックで完結させる
業務アプリの大半は「一覧 → 作成 → 編集 → 削除」の CRUD と「フォームを投げる」の繰り返しです。本記事ではこれを Hono + Inertia + React の上で、できるだけ少ないコードと少ない概念で実装するパターンを示します。組み合わせる主要ライブラリ:
- Drizzle ORM: TypeScript ファーストの軽量ORM。SQL に近く型安全
- Zod: スキーマ定義 + 型生成 + 入力検証を一発で
- Inertia useForm: クライアント側のフォーム状態とサーバ送信・エラー表示を一体化
全体の流れ
Step 1: スキーマを Zod で定義
「ひとつの真実の源」をZodスキーマにするのが2026年のベストプラクティスです。例:ユーザー作成フォーム。
- email: 文字列、メール形式
- name: 文字列、1〜80文字
- role: 'admin' | 'editor' | 'viewer'
Zod の z.object({...}) で書き、z.infer<typeof Schema> で TypeScript 型を生成。サーバ側のバリデーションとクライアント側の型を1ヶ所で管理できます。スキーマは shared/ 等のディレクトリに置き、サーバ・クライアント両方から import できる構造に。
Step 2: Drizzle で DB スキーマを定義
Drizzle ORM は TypeScript で SQL を書く感覚に近いORMです。schema.ts にテーブル定義を書き、マイグレーションは drizzle-kit generate / drizzle-kit push で生成・適用します。
Drizzle のテーブル定義から得られる型を、Zod スキーマと突き合わせるユーティリティ(drizzle-zod パッケージ)も便利です。これで「DB列定義 ⇄ Zod ⇄ TypeScript型」の三角形が自動同期できます。詳細は Drizzle ORM 公式 を参照してください。
Step 3: Hono ハンドラ — index / create / update / delete
業務アプリで最低限欲しいエンドポイントを Hono で書きます。
- GET /users: 一覧。Drizzle で select() し、Inertia レスポンスでページ Users/Index に props として配列を返す
- GET /users/new: 空フォーム表示
- POST /users: Zod で c.req.parseBody() をパース → 失敗ならエラーを返却、成功なら Drizzle で insert() → redirect('/users') + フラッシュメッセージ
- PATCH /users/:id: 編集処理(同じくZod→Drizzle→redirect)
- DELETE /users/:id: 削除
Inertia は POST 後に redirect を返すと SPA 内でのナビゲーションを自動処理します。「PRG パターン(Post-Redirect-Get)」を素直に使えるのがこのスタックの大きな強みです。
Step 4: React 側 — useForm でフォームを書く
Inertia の useForm フックは、フォーム状態 / 送信中フラグ / サーバから返ったエラーを一括で扱える便利なフックです。
- data: フォームの値
- setData(key, value): 値の更新
- post(url) / put(url) / delete(url): 送信
- processing: 送信中フラグ
- errors: サーバ側 Zod が返したエラー(フィールド名 → メッセージ)
UI 側では errors.email を <input> の下に表示するだけで、サーバ側エラーがそのまま画面に反映されます。React Hook Form を別途入れなくても、十分な実用性が出ます。
Step 5: フラッシュメッセージと共有プロパティ
「保存しました」「削除しました」のようなトースト通知は、Inertia の共有プロパティで実装すると綺麗です。
1. Hono ミドルウェアで、リクエスト処理後にセッションから flash を読み出して共有プロパティに乗せる
2. React 側のレイアウトコンポーネントで usePage().props.flash を見てトースト表示
これで「サーバ側で redirect + flash('saved')」を呼ぶだけで、画面右上にトーストが出る統一動作が組めます。
実装上の注意点
- N+1 問題: 一覧 + 関連データを取るときは Drizzle の with で eager load するか、leftJoin を使う
- サーバ側 Zod を信頼の唯一の源に: クライアント側にも同じZodスキーマを使ってリアルタイム検証できますが、最終判定はサーバ側
- 大きいフォーム: ファイルアップロードを含む場合は useForm の transform と multipart/form-data を使う
- 楽観的更新: Inertia の preserveScroll: true、only: ['users'] 等の partial reload オプションで体験を磨ける
- トランザクション: 複数テーブルの整合性が必要な処理は Drizzle のトランザクションでまとめる
次回 — 認証
次回は 認証完全ガイド で、Better Auth / Lucia / 自前セッション の選び方と Inertia 統合のパターンを扱います。シリーズ全体は Part 1: スタック概要 を参照ください。
FAQ
Q1: Drizzle じゃなくて Prisma でもいいですか?
A: 使えます。ただ Workers 環境ではバンドルサイズと cold start の観点で Drizzle の方が有利です。Node 環境なら Prisma も実用的です。
Q2: React Hook Form を入れる価値はありますか?
A: 大規模で複雑なフォームでは検討の価値あり。中小規模では useForm 単体で十分なケースが多いです。
Q3: 楽観的更新はどうやる?
A: Inertia の transform と React 側の状態更新を組み合わせるか、TanStack Query を併用するパターンがあります。やりすぎないことが重要。
Q4: ファイルアップロードは?
A: useForm は multipart/form-data をサポートしているため、<input type="file" /> の値を setData に渡すだけで動きます。
参考文献
お気軽にご相談ください
お問い合わせ