Zod Part 1: Stop Writing Validation by Hand
You're building a signup form. A user fills in their name, email, and age. You grab those values and send them to your backend. Simple enough.
But what if they type "abc" where you expected a number? What if the email field is blank? What if they send extra fields you never asked for?
Without any guardrails, that bad data quietly makes its way into your app — and you find out something went wrong three steps later, with no clear clue about what broke or where.
So you write validation by hand. And it looks something like this:
📝 Note: This post was written with the assistance of AI. The content reflects my personal learning and understanding and should not be taken as professional advice. Please do your own research and due diligence before acting on anything written here.
😬 The Mess Without Zod
function validateUser(data) {
if (!data.name || typeof data.name !== "string") {
throw new Error("Name is required and must be a string");
}
if (!data.email || !data.email.includes("@")) {
throw new Error("Email is invalid");
}
if (!data.age || typeof data.age !== "number" || data.age < 18) {
throw new Error("Age must be a number and at least 18");
}
// ...and this is just three fields
}
This gets the job done — barely. But it has real problems:
- It throws one error at a time. The user fixes the name, submits again, and only then finds out the email is also broken.
- It gives you no TypeScript types. You still have to describe your data separately if you want type safety.
- Add five more fields and this function becomes a wall of
ifstatements nobody wants to touch.
There is a better way.
🧱 What is Zod?
Zod is a JavaScript and TypeScript library that lets you describe the shape of data you expect — and then automatically checks whether real data matches that shape.
Think of Zod as a bouncer at a club. You tell the bouncer the rules once: "Only let in people who are 18+, have a valid email, and have a name." Zod enforces those rules every single time data comes through — no
ifstatements required.
Install it with:
npm install zod
📐 Step 1 — Define a Schema
Everything in Zod starts with a schema — a description of what valid data looks like.
import { z } from "zod";
// Describe the shape you expect
const userSchema = z.object({
name: z.string(), // must be text
email: z.string().email(), // must be text AND a valid email format
age: z.number().min(18), // must be a number, and at least 18
});
That's your rulebook. Zod now knows exactly what a valid user looks like.
✅ Step 2 — Validate Data with safeParse
Once you have a schema, you pass real data through it using safeParse:
const result = userSchema.safeParse({
name: "Priya",
email: "priya@example.com",
age: 25,
});
if (result.success) {
console.log("Valid data:", result.data); // safe to use
} else {
console.log("Errors:", result.error.errors); // tell the user what went wrong
}
safeParse never crashes your app. It always hands you back a clean result object — either { success: true, data: ... } or { success: false, error: ... }. You decide what happens next.
❌ Step 3 — Reading Errors When Validation Fails
Now let's deliberately send bad data and see what Zod tells us:
const result = userSchema.safeParse({
name: "Priya",
email: "not-an-email", // ❌ invalid format
age: 15, // ❌ under 18
});
if (!result.success) {
result.error.errors.forEach((err) => {
console.log(`Field: ${err.path[0]} | Problem: ${err.message}`);
});
}
// → Field: email | Problem: Invalid email
// → Field: age | Problem: Number must be greater than or equal to 18
Two things worth noticing here:
- Zod catches all errors at once — not just the first one. That's exactly what you need for a form, where you want to highlight every broken field in one go.
err.pathtells you which field failed, anderr.messagetells you why — making it easy to show the right error next to the right input.
You can also write your own error messages instead of using Zod's defaults:
age: z.number().min(18, { message: "You must be at least 18 to sign up" })
🆚 But Wait — What About parse Instead of safeParse?
Good question. Zod gives you two ways to validate:
safeParse() | parse() | |
|---|---|---|
| On invalid data | Returns { success: false } | Throws an error — app crashes |
| On valid data | Returns { success: true, data } | Returns data directly |
| Best for | Forms — show errors gracefully | When you want validation failures to throw loudly |
// safeParse — you stay in control
const result = userSchema.safeParse(formData);
if (result.success) {
console.log(result.data); // safe to use
}
// parse — gives you data directly, but throws on failure
try {
const data = userSchema.parse(formData); // throws if invalid
console.log(data);
} catch (err) {
console.log(err.errors);
}
The rule of thumb: use safeParse when you want to handle errors yourself gracefully — use parse when you want the app to throw loudly the moment something invalid comes through.
🎯 What You Can Do Now
- Define a schema using
z.object()with typed fields likez.string(),z.number(), andz.string().email() - Add constraints like
.min(),.max(), and.length()to tighten your rules - Use
safeParseto validate data without crashing your app - Read
result.error.errorsto get every broken field and its message in one shot - Use
parsewhen you want validation failures to throw loudly and stop execution immediately
The shift that happens when you start using Zod: you stop thinking about validation as a bunch of
ifstatements scattered around your code, and start thinking about it as a single source of truth for what your data is allowed to look like.
⏭️ Next Up
In Part 2, we go deeper into schemas — how to write custom validation rules with .refine() and superRefine, how to handle nested objects, and how to compose schemas together using .omit(), .pick(), and .extend().

