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

Forms & CRUD Patterns in Hono + Inertia + React — Drizzle ORM, Zod, and Inertia useForm [2026]

How to build CRUD and forms — the heart of internal-tool work — in Hono + Inertia + React. Covers Drizzle ORM for DB access, Zod for end-to-end typed validation, and Inertia's useForm for error display, redirects, and flash messages, all from a practitioner's perspective.


Goal — keep the heart of business apps in one stack

Most business apps are loops of "list → create → edit → delete" CRUD and "submit a form." This article shows how to do that on Hono + Inertia + React with as little code and as few moving parts as possible. The combo: - Drizzle ORM: TypeScript-first lightweight ORM, close to SQL, type-safe - Zod: schema definition + type inference + validation, all in one - Inertia useForm: client-side form state + submit + error display, unified

End-to-end flow

Loading diagram...

Step 1: define your schema in Zod

Make a single source of truth in Zod. Example: a user-create form. - email: string, email format - name: string, 1–80 chars - role: 'admin' | 'editor' | 'viewer' Write it as `z.object({...})` and derive the TypeScript type with `z.infer<typeof Schema>`. Server validation and client types share one definition. Place schemas under `shared/` so server and client can both import them.

Step 2: define DB schema with Drizzle

Drizzle ORM lets you write SQL-flavored TypeScript. Define tables in `schema.ts`; generate / apply migrations with `drizzle-kit generate` / `drizzle-kit push`. The `drizzle-zod` package bridges Drizzle table types to Zod schemas, keeping the DB column definitions, Zod, and TypeScript types in sync automatically. See Drizzle ORM official for details.

Step 3: Hono handlers — index / create / update / delete

Minimum endpoints for a business app, in Hono: - GET `/users`: list — `select()` via Drizzle, return as Inertia props on page `Users/Index` - GET `/users/new`: empty form - POST `/users`: parse `c.req.parseBody()` with Zod → on failure return errors; on success `insert()` via Drizzle → `redirect('/users')` + flash - PATCH `/users/:id`: edit (Zod → Drizzle → redirect) - DELETE `/users/:id`: delete Inertia handles redirects after POST as in-app navigation. Plain Post-Redirect-Get works out of the box — a real strength of this stack.

Step 4: React side — useForm

Inertia's `useForm` packages form state, submit-in-flight flag, and server errors: - `data`: form values - `setData(key, value)`: update - `post(url)` / `put(url)` / `delete(url)`: submit - `processing`: submit-in-flight flag - `errors`: Zod errors from the server (field → message) In the UI, render `errors.email` under the email `<input>` and you're done — server errors flow straight to the screen. You can ship without React Hook Form for most pages.

Step 5: flash messages via shared props

"Saved" / "Deleted" toasts are clean to implement via Inertia shared props. 1. After each request, server middleware reads `flash` from the session and pushes it onto shared props 2. The React layout component reads `usePage().props.flash` and renders a toast Now `redirect + flash('saved')` on the server is enough to pop a toast in the top-right of the UI consistently.

Things to watch

- N+1: when listing with relations, eager-load via Drizzle's `with` or use `leftJoin`. - Trust the server Zod: client-side Zod is fine for instant feedback, but the final word is server-side. - Large forms: include file uploads via `useForm`'s `transform` and `multipart/form-data`. - Optimistic UI: Inertia's partial-reload options like `preserveScroll: true` / `only: ['users']` polish the feel. - Transactions: wrap multi-table integrity in Drizzle transactions.

Next up — authentication

Part 4 is the authentication guide — Better Auth / Lucia / DIY sessions and how they integrate with Inertia. Series overview: Part 1.

FAQ

Q1: Can I use Prisma instead of Drizzle? A: Yes. On Workers, Drizzle wins on bundle size and cold start. On Node, Prisma is fine. Q2: Is React Hook Form worth adding? A: For very large / complex forms, maybe. For most business pages, `useForm` alone is enough. Q3: How do I do optimistic updates? A: Combine Inertia's `transform` with local React state, or layer in TanStack Query. Don't overdo it. Q4: File uploads? A: `useForm` supports `multipart/form-data` — `setData` with the file input value just works.

References

Feel free to contact us

Contact Us