Inline register() validation rules work, but they're scattered and disconnected from your TypeScript types. Zod declares an entire form's shape and validation rules in one schema, with matching types inferred automatically. This lesson covers schemas, type inference, and connecting Zod to React Hook Form.
1Validation Rules, Scattered vs. Declared
Inline validation rules passed to register() work but end up scattered across a form's JSX, hard to reuse, and disconnected from TypeScript types. Zod instead declares an entire data shape and its validation rules in a single schema object, readable top to bottom and reusable anywhere.
2Defining a Schema
A Zod schema is built by chaining validators, like z.string().email() or z.number().positive(), combined into z.object({...}) to describe an entire form's shape and rules in one readable declaration.
3Inferring TypeScript Types from a Schema
z.infer<typeof schema> generates a TypeScript type directly from a Zod schema, so runtime validation and compile-time types are derived from the exact same source, eliminating the risk of manually maintained duplicate type definitions silently drifting out of sync.
4Connecting Zod to React Hook Form
The @hookform/resolvers/zod package bridges the two libraries: passing zodResolver(schema) to useForm's resolver option makes React Hook Form's validation entirely schema-driven, replacing scattered per-field rules with error messages sourced directly from the schema.
5Validation Beyond Forms
Zod isn't limited to form validation — the same schema can validate an API response's shape, environment variables, or any other untrusted external data, offering one consistent validation library and mental model used throughout an entire application.
6Step-by-Step Breakdown
Validation Rules, Scattered vs. Declared. Inline register('email', { required, pattern }) rules work, but they're scattered across your JSX, hard to reuse, and disconnected from your TypeScript types. Zod lets you declare your entire data shape and its validation rules in ONE schema object — readable top to bottom, and reusable anywhere.
Defining a Schema. A Zod schema is built by chaining validators: z.string().min(2), z.string().email(), z.number().positive(). Combine several field schemas into z.object({...}) to describe an entire form's shape and rules in a single, readable declaration.
What's the main organizational advantage of a Zod schema over inline register() rules scattered across each input?
- →The entire shape and its rules are declared in one reusable, readable place
- →It makes form submission execute noticeably faster
Inferring TypeScript Types from a Schema. z.infer<typeof schema> generates a TypeScript type directly from your Zod schema — your runtime validation and your compile-time types come from the exact same source, so they can never silently drift out of sync the way manually maintained duplicates can.
Connecting Zod to React Hook Form. @hookform/resolvers/zod bridges the two libraries: pass zodResolver(schema) to useForm, and React Hook Form's validation is now driven entirely by your Zod schema — no more required/pattern scattered in register calls, and your formState.errors messages come straight from the schema.
After wiring resolver: zodResolver(signupSchema) into useForm, where should new validation rules for the email field now be added?
- →Directly in the Zod schema's email field definition
- →Back in register('email', { ... }) as before
Validation Beyond Forms. Zod isn't limited to forms — the exact same schema can validate an API response's shape, environment variables, or any untrusted external data. This is often the bigger long-term value: one validation library, one mental model, used consistently across your entire application.
Mastery Achieved. You now understand Zod: declaring validation rules and shape in one schema, inferring matching TypeScript types automatically, connecting it to React Hook Form with zodResolver, and reusing the same schema for validating API responses or any other untrusted data. This closes out Advanced Forms — next, you'll move into React Architecture.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Write Zod Error Messages That Read Well to Assistive Technology
Since zodResolver's error messages flow directly into formState.errors and typically get announced via aria-live regions, write custom Zod error messages (like .email('Please enter a valid email')) in clear, complete sentences rather than terse technical fragments.
SEO Implications
- 1
Schema Validation Can Also Protect Server-Rendered Data Paths
Using the same Zod schema to validate incoming data on a server (e.g. a Server Action or API route) as well as the client form ensures consistent validation regardless of how the request reaches the server, protecting data integrity that indirectly supports reliable server-rendered content.
Best Practices
Define One Schema and Reuse It Across Form, API, and Type Definitions
Avoid maintaining a separate hand-written TypeScript interface alongside a Zod schema for the same data — use z.infer to derive the type, keeping a true single source of truth.
Write Custom, User-Friendly Error Messages in the Schema
Zod's default error messages are technically accurate but not always ideal for end users — pass custom messages to validators (like z.string().min(8, 'Password must be at least 8 characters')) for clearer form feedback.
Frequent Bugs
A form's TypeScript type and its actual runtime validation rules have drifted out of sync after an update.
This typically happens when a schema and a hand-written interface are maintained separately. Replace the manual interface with type SignupForm = z.infer<typeof signupSchema> so the type always reflects the schema exactly.
Adding a new validation rule to register() has no effect after adopting zodResolver.
Once a form uses zodResolver, React Hook Form's validation is driven entirely by the schema — rules must be added to the Zod schema itself, not to register()'s second argument, which is ignored when a resolver is set.
Real-World Examples
A Signup Form Validated End-to-End with One Schema
A signup form needs client-side validation, a matching TypeScript type for the submit handler, and server-side validation of the same data when it reaches the API. Defining one Zod schema, used via zodResolver on the client and schema.safeParse() on the server, guarantees both sides enforce identical rules with zero duplicated logic.
const signupSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'At least 8 characters'),
});
type SignupForm = z.infer<typeof signupSchema>;
// Client: useForm({ resolver: zodResolver(signupSchema) })
// Server: const result = signupSchema.safeParse(await request.json());