Form Validation in Next.js with Zod: Client to Server
By Arash Latifi
Practical guide to validating Next.js forms with Zod and React Hook Form — one shared schema for client and Server Actions, clean errors and solid UX.
TL;DR: Make one
Zodschema the source of truth and reuse it withReact Hook Formon the client and inServer Actionson the server. Fast feedback, identical errors and real security.
Why Zod for Next.js forms
Client-only validation is insecure and server-only feels slow. A single Zod schema gives you types and runs everywhere. It pairs with Server Actions and useActionState and keeps messages consistent.
For boundaries see RSC poisoning and use-client boundaries and caching and PPR in Next.js 15.
One shared schema
Create schemas/contact.ts and import it everywhere.
One schema, one place to change rules, types via z.infer.
Client: React Hook Form + zodResolver
Use mode: "onBlur" to reduce noise and disable the button while submitting.
Server: validation with Server Actions
Never trust the client. Re-run the same schema on the server.
Works well with Persian RTL optimization in Next.js 15.
Tool comparison
| Tool | Strengths | Best for | Note |
| --- | --- | --- | --- |
| Zod + React Hook Form | Strong types, shared schema, huge ecosystem | Common to complex forms | Don't forget zodResolver |
| Valibot | Smaller bundle, faster | Bundle-sensitive apps | Different API |
| Yup | Mature | Legacy migration | Weaker types |
For most Next.js forms Zod wins on docs and ecosystem.
Implementation checklist
- [ ] Schema in one shared file, imported on client and server
- [ ] Error messages short and identical on both sides
- [ ] Inputs have
maxlimits and trim whitespace - [ ]
aria-invalidset for accessibility - [ ] Submit button disabled during
isSubmittingandpending - [ ] Server uses
safeParseand returnsfieldErrors - [ ] Manual tests with empty and invalid inputs pass
- [ ] Server logs failures without leaking sensitive data
FAQ
Is client validation enough? No. Always re-run the same schema on the server.
Where should I keep the Zod schema?
In schemas/ and reuse the single contactSchema export everywhere.
How do I validate file uploads?
Use z.instanceof(File).refine(f => f.size < 2_000_000, "File too large") and repeat checks on the server.