On this Page
On this Guide
- Lesson 13: Validating a User Registration Form
- Lesson 14: Validating API Query Parameters
- Lesson 15: Validating Environment Variables
- Lesson 16: Handling Nested Errors and Formatted Output
- Lesson 17: Reusable Zod Error Parser for Forms
- Lesson 18: Blog Post Editor β Real-World Case Study
- Lesson 19: Zod + React Hook Form Integration
- Lesson 21: Validating Express Requests with Zod
- Lesson 22: Custom Zod Error Formatter for Express APIs
- Lesson 20: Final Thoughts, Best Practices, and Resources
The Scenario
You’re building a blog editor where authors submit the following data:
title: required, min 5 charsbody: required, min 50 charstags: array of unique strings (1β5 items)metadata: includespublished,publishedAt, andslug- If
publishedistrue,publishedAtandslugmust be set
Letβs build a robust schema for this.
Building the BlogPostSchema
import { z } from "zod";
export const BlogPostSchema = z
.object({
title: z.string().min(5, "Title must be at least 5 characters"),
body: z.string().min(50, "Content must be at least 50 characters"),
tags: z
.array(z.string().min(2))
.min(1)
.max(5)
.refine((tags) => new Set(tags).size === tags.length, {
message: "Tags must be unique",
}),
metadata: z.object({
published: z.boolean(),
publishedAt: z.string().optional(), // ISO string expected
slug: z.string().optional(),
}),
})
.superRefine((data, ctx) => {
if (data.metadata.published) {
if (!data.metadata.publishedAt) {
ctx.addIssue({
path: ["metadata", "publishedAt"],
code: "custom",
message: "Published date is required when published",
});
}
if (!data.metadata.slug) {
ctx.addIssue({
path: ["metadata", "slug"],
code: "custom",
message: "Slug is required when published",
});
}
}
});
Handling Tags and Rich Content
Zod lets you validate both structure and content:
tags: z
.array(z.string().min(2, "Each tag must be at least 2 characters"))
.min(1)
.max(5)
Add .refine() to enforce uniqueness.
Validating Nested Metadata
The metadata object is fully typed and validated:
metadata: z.object({
published: z.boolean(),
publishedAt: z.string().optional(),
slug: z.string().optional(),
})
Use .superRefine() for cross-field logic like:
- If
publishedistrue, then bothpublishedAtandslugmust be filled.
Optional Draft Logic with Refinement
Want to allow drafts with just title and body?
if (!data.metadata.published) {
// Allow missing `publishedAt` and `slug`
}
Thatβs the beauty of .superRefine() β you control the logic, not just the shape.
Summary
- This schema combines strings, arrays, nested objects, optional fields, and cross-field rules
- Zod helps enforce business logic cleanly and predictably
- Perfect for CMS, editors, admin panels, or custom content tools
Next: Lesson 19 β Zod + React Hook Form Integration for Full Form Validation
Master the Code, Be the Guru!