Module 6.3: Advanced Validation with Zod

🎯 Lesson Objectives
After completing this lesson, you will master:
- Schema Validation: Shift your
mindsetto centralizedSchema Validationinstead of scatteredif/elsechecks. - Zod: A powerful, TypeScript-first
validation library. - Integration: Seamlessly connect Zod with React Hook Form using
@hookform/resolvers. - Complex Rules: Tackle challenging
validationscenarios like Password Confirmation andCustom Validation.
1. Why Schema Validation?
In the previous lesson, we approached validation by passing rules directly to individual register functions. While this works fine for small forms, it quickly becomes challenging with larger ones:
Validation logicbecomes scattered across your tSX files.- Reusing
rules(e.g., an emailruleneeded in multiple places) is cumbersome. Cross-field validation(e.g., ensuringConfirm PasswordmatchesPassword) is difficult toimplement.
Zod empowers you to define all validation rules (the Schema) for your form in one centralized place.

2. Installation
You’ll need to install zod and the @hookform/resolvers adapter to integrate with React Hook Form.
npm install zod @hookform/resolvers
# hoặc
yarn add zod @hookform/resolvers
# hoặc
pnpm add zod @hookform/resolvers
# hoặc
bun add zod @hookform/resolvers3. Zod Essentials (Core Concepts)

Zod goes far beyond basic data types. Here are the core functionalities you need to master:
3.1 Primitives & Coercion
HTML input elements always return strings, even when type="number". Zod provides z.coerce to automatically cast types before validation.
const basicSchema = z.object({
// String Validation
username: z.string().trim().min(3).max(20),
// Number Validation & Coercion
// Tự động chuyển chuỗi "25" thành số 25
age: z.coerce.number().min(18).int().positive(),
// Boolean
isActive: z.boolean().default(false),
// Date
birthDate: z.coerce.date(),
});3.2 Common Validations (Popular Rules)
Zod comes packed with a long list of commonly used validation rules:
- String:
.email(),.url(),.uuid(),.regex(regex),.startsWith(),.endsWith(). - Number:
.gt(5)(greater than),.gte(5)(greater than or equal),.lt(5)(less than),.lte(5)(less than or equal). - Optional/Nullable:
.optional(): Allowsundefined(if not submitted)..nullable(): Allowsnull..nullish(): Allows bothnullandundefined.
const profileSchema = z.object({
website: z.string().url().optional().or(z.literal('')), // URL hoặc chuỗi rỗng
bio: z.string().max(100).nullish(), // Có thể null hoặc không có
});3.3 Enums & Literals (Fixed Lists)
Ideal for Dropdown (Select) or Radio Button components.
// Enums cho Select
const RoleEnum = z.enum(['ADMIN', 'USER', 'GUEST']);
type Role = z.infer<typeof RoleEnum>; // 'ADMIN' | 'USER' | 'GUEST'
// Literals cho giá trị chính xác
const termsSchema = z.object({
agreed: z.literal(true, {
errorMap: () => ({ message: 'Agreement is required' }),
}),
});4. Zod Advanced Patterns (Advanced Techniques)
4.1 Refine (Custom Validation)
When built-in rules aren’t enough, .refine() is your go-to. It’s Zod’s most powerful tool for crafting custom validation logic.
const passwordSchema = z.string().refine((val) => !val.includes('123456'), {
message: "Password cannot contain the string '123456'",
});
// Async Refine (Kiểm tra Server)
const emailSchema = z
.string()
.email()
.refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{
message: 'Email is already in use',
}
);4.2 Cross-Field Validation
Use .refine() or .superRefine() at the object level to compare fields with each other (e.g., Confirm Password, Start Date < End Date).
const changePassSchema = z
.object({
password: z.string(),
confirm: z.string(),
})
.refine((data) => data.password === data.confirm, {
message: 'Passwords do not match',
path: ['confirm'], // Chỉ định lỗi hiển thị ở trường 'confirm'
});4.3 Transformations (Data Manipulation)
Use .transform() to modify the output data after successful validation.
const dataSchema = z
.object({
// Tự động viết hoa tên khi submit
name: z.string().transform((val) => val.toUpperCase()),
// Tính toán giá trị mới
price: z.number(),
quantity: z.number(),
})
.transform((data) => ({
...data,
total: data.price * data.quantity, // Thêm trường total tự động
}));4.4 Error Handling
Customize error messages, either globally or for specific fields.
// Cách 1: Custom message inline
z.string().min(1, { message: 'This field cannot be empty' });
// Cách 2: Custom Error Map (Toàn cục)
// (Thường dùng trong file setup của dự án)
z.setErrorMap((issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
return { message: 'Invalid data type' };
}
return { message: ctx.defaultError };
});5. Integrating Zod with React Hook Form
Leverage zodResolver within useForm.
import { useForm, type SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const signUpSchema = z.object({
fullName: z.string().min(1, 'Full name is required'),
email: z.string().email('Invalid email format'),
password: z.string().min(6, 'Password must be at least 6 characters'),
});
type SignUpFormValues = z.infer<typeof signUpSchema>;
export default function SignUpForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SignUpFormValues>({
resolver: zodResolver(signUpSchema),
});
const onSubmit: SubmitHandler<SignUpFormValues> = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)} className="p-4 space-y-4">
<div>
<input {...register('fullName')} className="border p-2 w-full" placeholder="Full Name" />
{errors.fullName && <p className="text-red-500">{errors.fullName.message}</p>}
</div>
<div>
<input {...register('email')} className="border p-2 w-full" placeholder="Email" />
{errors.email && <p className="text-red-500">{errors.email.message}</p>}
</div>
<div>
<input type="password" {...register('password')} className="border p-2 w-full" placeholder="Password" />
{errors.password && <p className="text-red-500">{errors.password.message}</p>}
</div>
<button type="submit" className="bg-blue-600 text-white p-2 rounded">
Sign Up
</button>
</form>
);
}6. Hands-On Labs
Lab 1: Tactical Access (Security Setup)
Build a security system with a Tactical Industrial design language (High Contrast).
- Features:
Entropy Monitor: A password strength meter using a “Segmented Bar” (Industrial Style).Mechanical Toggle: A mechanical toggle switch to replace standard checkboxes.Strict Validation: Instanterror feedbackwith a warningUI.
- Techniques: Employ
z.boolean().refine()for the toggle,useWatchto calculate strength, and pure CSS for the mechanicalelements.

💡 Tips:
- To compare
passwordandconfirmPassword, use.refine()at the top-level of yourSchema(theobjectlevel).- For the “Accept Terms” checkbox, Zod defaults to a
boolean; use.refine(val => val === true)to enforce selection.- Extract the
StrengthMeterinto a separatecomponentand useuseWatchto independently monitor thepasswordfield, preventing unnecessary fullformre-renders.
Lab 2: Swiss Invoice (Minimalist Document)
Create an invoice generator with a Swiss Design aesthetic (Typography-centric), embodying absolute minimalism.
- Features:
Editorial Preview: A previewinterfacethat resembles a high-end magazine page or printed document.Realtime Calculation: Automatically calculate the total amount asquantityor unit price changes.Dynamic Items: LeverageuseFieldArray(covered in Module 6.2) for automatic row addition/removal.
- Techniques:
useFieldArraycombined with azodarray schema, andwatchto calculatedata.

💡 Tips:
- Define your
schemausingz.array()for youritems.- Use
useWatchto extractquantityandpricevalues fortotalcalculations.- For a valid
date stringfrom aninput type="date", a simplez.string()validationis sufficient.