Skip to main content
株式会社オブライト
Web Development2026-04-244 min read

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-datasetData with the file input value just works.

References

Feel free to contact us

Contact Us