株式会社オブライト
Web Development2026-04-24

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: クライアント側のフォーム状態とサーバ送信・エラー表示を一体化

全体の流れ

Loading diagram...

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` に渡すだけで動きます。

参考文献

お気軽にご相談ください

お問い合わせ