Forms & Validation (Zod)
Relevant source files
The following files were used as context for generating this wiki page:The project employs a robust and type-safe system for handling forms and data validation, centered around react-hook-form for state management and zod for schema-based validation. This architecture is complemented by a set of reusable UI components built on shadcn/ui and Radix UI, ensuring a consistent and efficient development experience for creating forms across different features like product management, user administration, and checkout processes.
This system is not limited to front-end forms; zod is also integral to the application's data integrity strategy, where it is used to validate the structure and types of incoming API responses. This dual-purpose use of zod provides end-to-end type safety, from user input on the client to data consumption from the backend.
Core Technologies
The form and validation system is built upon a stack of several key libraries that work in concert.
| Library/Module | Role | Source File |
|---|---|---|
react-hook-form | Manages form state, handles submissions, and tracks validation errors with minimal re-renders. | features/products/components/admin/product-form.tsx:23 |
zod | Defines validation schemas for form data and API responses, providing static type inference. | features/products/schemas/product-form.schema.ts:6 |
@hookform/resolvers/zod | A resolver that connects react-hook-form to zod schemas, enabling validation based on the defined schema. | features/products/components/admin/product-form.tsx:6 |
components/ui/form.tsx | Provides a set of reusable, accessible form components (<Form>, <FormField>, etc.) based on shadcn/ui and Radix UI. | components/ui/form.tsx |
lib/validation-utils.ts | Contains utility functions for validating raw API response data against zod schemas at runtime. | lib/validation-utils.ts |
This diagram illustrates the relationship between the core technologies in the form system.
Schema Definition with Zod
Validation logic is centralized and declared using zod schemas. These schemas define the shape, data types, and constraints for a given data structure, and they are the single source of truth for validation rules.
Sources: features/products/schemas/product-form.schema.ts, lib/validations/forms.ts
Basic and Custom Validations
Schemas are constructed by composing zod's built-in validation types. The productFormSchema is a comprehensive example of this.
- Standard Types:
z.string(),z.number(),z.boolean(). - Constraints:
.min(),.max(),.positive(),.nonnegative(),.int(),.regex(). - Modifiers:
.optional(),.nullable(),.default(). - File Validation: A combination of
z.instanceof(File)and.refine()is used to validate file types and sizes.
// features/products/schemas/product-form.schema.ts:11-20
const imageFileSchema = z
.instanceof(File, { message: "Vui lòng chọn một file ảnh" })
.refine(
(file) => file.size <= 5 * 1024 * 1024, // 5MB
"Kích thước file không được vượt quá 5MB",
)
.refine(
(file) => ["image/jpeg", "image/png", "image/webp"].includes(file.type),
"Chỉ chấp nhận file ảnh định dạng JPEG, PNG hoặc WebP",
);
Sources: features/products/schemas/product-form.schema.ts:11-20
Schema Composition and Extension
Zod schemas can be extended to create more specific versions for different use cases, such as create vs. update operations. The createProductFormSchema extends the base productFormSchema to make the image field mandatory.
This diagram shows how createProductFormSchema and updateProductFormSchema inherit from and modify the base productFormSchema.
Sources: features/products/schemas/product-form.schema.ts:88-103
Example Schema: Product Form
The productFormSchema defines the validation rules for the product creation and editing form.
| Field | Zod Type | Description |
|---|---|---|
name | z.string().min(2).max(200) | Product name, required. |
priceInt | z.number().int().positive().min(1000) | Product price in VND, must be a positive integer. |
description | z.string().max(5000).optional().nullable() | Optional product description. |
categoryId | cuidSchema().optional().nullable() | Optional foreign key for the category. |
image | imageFileSchema.optional().nullable() | Optional main product image file, with size and type validation. |
tryOnImage | tryOnImageFileSchema.optional().nullable() | Optional PNG image for AR try-on, with specific size and type validation. |
stock | z.number().int().nonnegative().optional().nullable() | Optional stock quantity. |
slug | z.string().max(200).regex(/^[a-z0-9-]+$/).optional().nullable() | Optional URL-friendly slug. |
Sources: features/products/schemas/product-form.schema.ts:38-86
Form Implementation
Form components integrate the schema, react-hook-form, and UI components to create a complete user-facing form. The ProductForm and UserEditFormDialog are primary examples of this pattern.
Sources: features/products/components/admin/product-form.tsx, features/users/components/user-edit-form-dialog.tsx
Form Setup
The useForm hook from react-hook-form is the entry point. It's configured with a zodResolver that points to the corresponding validation schema.
// features/users/components/user-edit-form-dialog.tsx:47-56
const form = useForm<UserEditFormValues>({
resolver: zodResolver(userEditFormSchema),
defaultValues: {
fullName: "",
phone: "",
role: UserRole.CUSTOMER,
isActive: true,
},
});
Sources: features/users/components/user-edit-form-dialog.tsx:47-56
UI Structure
The UI is built using components from components/ui/form.tsx.
- The
<Form>component acts as a context provider for theuseForminstance. - The
<FormField>component connects a specific form field toreact-hook-form's state management, handling value,onChange,onBlur, and error states. - Standard input elements like
<Input>and<Select>are placed inside<FormControl>. <FormMessage />automatically displays validation errors for the associated field.
// features/products/components/admin/product-form.tsx:81-94
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Tên sản phẩm *</FormLabel>
<FormControl>
<Input
placeholder="Nhập tên sản phẩm"
{...field}
disabled={isLoading}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
Sources: features/products/components/admin/product-form.tsx:81-94, components/ui/form.tsx
Data Submission Flow
The following diagram shows the sequence of events from user interaction to form submission.
This diagram illustrates the validation and submission process managed by react-hook-form and zod.
Sources: features/products/components/admin/product-form.tsx:77-79, features/users/components/user-edit-form-dialog.tsx:69-82
API Response Validation
Beyond forms, zod is also used to ensure the integrity of data received from the backend API. The lib/validation-utils.ts module provides helpers for this purpose.
validateResponse Utility
The validateResponse function is a crucial utility that attempts to parse unknown data (typically from an API response) against a provided zod schema. If validation fails, it throws a custom ValidationError containing detailed information about the mismatch, which is especially useful for debugging during development.
This flowchart shows the logic inside the apiGetValidated and validateResponse functions.
Sources: lib/validation-utils.ts:17-40, lib/api-client.ts:200-218
This runtime validation catches discrepancies between the frontend's expectations and the actual data structure returned by the backend, preventing potential runtime errors and ensuring data consistency throughout the client application.
// lib/api-client.ts:200-218
export async function apiGetValidated<T>(
endpoint: string,
schema: z.ZodSchema<T>,
config?: AxiosRequestConfig,
): Promise<T> {
const response = await apiGet<unknown>(endpoint, config);
// Log raw response in development for debugging
if (process.env.NODE_ENV === "development") {
console.warn(`[API Debug] GET ${endpoint} - Raw response:`, {
response,
type: typeof response,
isArray: Array.isArray(response),
keys:
typeof response === "object" && response !== null
? Object.keys(response)
: [],
});
}
return validateResponse(response, schema, `GET ${endpoint}`);
}
Sources: lib/api-client.ts:200-218
Common Preprocessing Utilities
To handle potential inconsistencies in data from the backend before validation, several z.preprocess functions are defined in types/common.types.ts. These functions clean up data, such as converting null or undefined image URL arrays to an empty array, ensuring that the subsequent validation step receives data in a predictable format.
preprocessImageUrls: Ensures the value is an array of strings, returning[]fornullorundefined.preprocessDateString: ConvertsDateobjects to ISO strings.preprocessNullableCuid: Handlesnull,undefined, or empty strings for optional CUID fields, returningnull.
Sources: types/common.types.ts:74-113
Conclusion
The project's approach to forms and validation is a cornerstone of its architecture, providing a scalable, maintainable, and type-safe system. By combining react-hook-form for efficient state management, zod for declarative and powerful schema validation, and a custom set of shadcn/ui components, the application ensures a high level of data integrity and a consistent developer experience. The extension of zod to validate API responses further strengthens this data integrity, creating a resilient client application that is robust against unexpected data structures from the backend.