Table of Contents
On the previous chapter I used a boring example for using Zod. It was boring because there was no external input involved. Zod is useful when we need to deal with data, whose shape (and therefore type) is only determined in runtime and the TypeScript compiler can’t keep us safe. A very common example is when handling incoming HTTP requests. For example:
const UserSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
async function createUser(req: Request, res: Response): Promise<void> {
const userData: User = UserSchema.parse(req.body);
const newUser = await saveUser(userData);
res.json(newUser);
}
We get two main benefits by using Zod:
- We can validate our input using Zod’s built-in and tested validation logic. The
createUser
function will fail if theemail
property contains a random string. -
userData
is typed. We don’t need to use casting (god forbid!) to work with the correct type in our application logic.
Why should I care about input validation?
Well… there are many good reasons but for starters it keeps your code secure, preventing code injection hacks and the like, but it also keeps you code less prone to unusual bugs. I feel like security has been discussed a lot so I’m not going to talk about that here. But the latter reason is as important IMHO.
There are a few questions we should ask ourselves:
Where do we rather catch an error?
I would argue that it’s best to catch an error as close as possible to the source of the error. If the code is very simple, that might not be important, but if we run complex logics and/or transactions, catching the error at the beginning might prevent weird or critical bugs related to state inconsistency etc.
What kind of error do we prefer?
If you’ve been working with JavaScript for a while you’re probably familiar with the infamous “Uncaught TypeError: Cannot Read Property of Undefined” error. I really hate when that happens! And really it could be easily avoided with Zod and the right schema.
Lets see this in action. First we’ll change our HTTP handler to not use Zod and run some basic logic:
async function createUser(req: Request, res: Response): Promise<void> {
const user: User = req.body;
processEmail(user);
res.status(200).end();
}
function processEmail(user: User) {
if (user.email.split("@")[1].endsWith("gmail.com") {
doSomethingGoogly(user);
} else {
doSomethingElse(user)
}
}
What kind of error would we get if req.body
doesn’t contain an email
?
TypeError: Cannot read properties of undefined (reading 'split')
Did I already mention that I hate this error?
What kind of error would we get with Zod?
ZodError: [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": [
"email"
],
"message": "Required"
}
]
Nice!
Summary
Zod becomes useful when you need to validate external input. One very common use case is when handling incoming HTTP requests. Zod is cool because it infers types out of schemas and allows you to use those types in your business logic. Assuming your schema is correct, the TypeScript compiler guarantees that correct input will be handled correctly. No surprises.
Input validation is important because it prevents bugs from happening deep in our code and provides meaningful error messages that’s easier to understand and solve.
Next chapter would be on how to sanitize incoming HTTP requests with Zod.