The article is strongly opinionated; it delves into the strengths of Zod and highlights the benefits of using it.
Why You Should Start Using Zod
If you haven't started using Zod yet, it's time to reconsider! This lightweight library significantly simplifies daily tasks, especially those involving validation and defining TypeScript
contracts. Zod not only makes things easier but also enhances maintainability and reduces boilerplate. Today, I'll give you a glimpse of the possibilities with Zod that can transform your coding experience.
The Problems Zod Solves
Imagine you have an API that creates a post. Without Zod, here's what the typical code might look like without Zod.
// models.ts file
interface PostData {
title: string;
content: string;
tags?: string[];
}
// validate-post-data.ts file
import { PostData } from "@models";
const validatePostData = (postData: PostData): string | null => {
if (!postData.title || postData.title.trim() === ``) {
return `Title is required and cannot be empty.`;
}
if (!postData.content || postData.content.trim() === ``) {
return `Content is required and cannot be empty.`;
}
if (postData.tags && postData.tags.some((tag) => tag.trim() === ``)) {
return `Tags cannot contain empty strings.`;
}
return null; // No errors
};
export { validatePostData };
// add-post.ts file
import axios from "axios"; // Ensure axios is installed (`npm install axios`)
import { PostData } from "@models";
const addPost = async (postData: PostData): Promise<void> => {
if (!validatePostData(postData)) throw Error("Validation error");
await axios.post(`https://example.com/api/posts`, postData);
};
export { addPost };
I've simplified this article by not delving into the presentation layer. What stands out immediately is the complexity of the validatePostData
function.
Additionally, any changes to the PostData
interface require us to consistently update the validatePostData
implementation as well.
What's more, what if one of our validators is flawed? A bug in a validator doesn't just mean maintenance headaches - it means that even if validation passes, we can't be certain the postData
truly matches the expected shape - at runtime it may be a null
or undefined
.
Zod offers a solution to these issues by reducing boilerplate, enhancing readability, and providing guaranteed type safety through defined schemas. Let's improve our code with Zod!
// models.ts file
// Ensure zod is installed (`npm install zod`)
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(1, 'Content is required'),
tags: z.array(z.string()).optional(),
}).strict();
type PostData = z.infer<typeof PostSchema>;
export { PostSchema };
export type { PostData };
// add-post.ts file
// Ensure axios is installed (`npm install axios`)
import axios from 'axios';
import { PostData } from "@models";
const addPost = async (postData: PostData): Promise<void> => {
// Validates the "postData"
// with Zod and throw an error if failed.
PostSchema.parse(postData);
await axios.post('https://example.com/api/posts', postData);
};
export { addPost };
A crucial feature of Zod is its initial check to ensure that postData
is actually an object. Then, it meticulously examines each value within the object. The PostSchema.parse
method runs this validation and will throw an error if anything is amiss or invalid.
The strict()
method in Zod ensures that an object contains only the specified properties. This is particularly useful for preventing the accidental sending of unnecessary data to the backend. You never know - there could be a bug on the backend side that mistakenly accepts and stores this extraneous data, posing a significant security risk.
Look how the IDE behaves. Instead of just showing an interface name as it would when you define types manually, it displays the exact object structure. This enhances the developer experience because you don’t always have to navigate to the interface file to see its layout.
Nice Developer Experience
To learn more about how Zod can enhance your TypeScript projects, check out this detailed article: Using Zod and TypeScript to Write Type-Safe Code.
Platform Agnosticism of Zod
The best part is that Zod is fully written in TypeScript. This allows you to create a schema library, extract a type from each schema, and then publish the library. As a result, you'll have the same declarations and contracts shared between the backend and frontend. When a schema changes, you can automatically push a new version of the contracts, update them in both projects and safely manage the migration to new contracts on both ends.
// Library code on npm
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(1, 'Content is required'),
tags: z.array(z.string()).optional(),
}).strict();
type PostData = z.infer<typeof PostSchema>;
export { PostSchema };
export type { PostData };
// Frontend app
import { PostData } from "@4markdown/contracts";
// Backend app
import { PostData } from "@4markdown/contracts";
Then, in package.json
, you can specify a dedicated version of your contracts library:
"dependencies": {
"@4markdown/contracts": "2.4.1",
To create such a library, you can use
nx
to maintain it easily. Here is an article on this topic: Publishing Nx Generated TypeScript Libraries on Npm.
An alternative setup with tRPC
is possible, but it's not part of this article.
The
tRPC
guarantees type safety for each API route and makes maintenance easier. It all depends on the stack you're currently using. Here you can read more about it tRPC.
Real Time Forms Validation
A common scenario involves showing results immediately when a user types or submits a form. With Zod, this becomes extremely easy and simplifies your code significantly. All you need to do is install dedicated adapters - for instance, if you're using React, it would be: npm install react-hook-form zod @hookform/resolvers
.
Then, simply define your schemas and enjoy the benefits of type safety.
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Define Zod schema
const loginSchema = z.object({
email: z.string().email("Invalid email format"),
password: z.string().min(5, "Password must be at least 5 characters")
});
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(loginSchema)
});
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="email">Email</label>
<input id="email" {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" type="password" {...register('password')} />
{errors.password && <p>{errors.password.message}</p>}
</div>
<button type="submit">Login</button>
</form>
);
}
export default LoginForm;
Just for your information, here is the size of the gzipped
version of each library taken from BundlePhobia during article creation:
- React Hook Form: 14.5 kB
- Zod: 9 kB
- @hookform/resolvers: 1 kB
However, it's a long-term investment. Without these tools, you would need to write your own code to handle such logic, and there's no guarantee that for larger applications, your custom solutions wouldn't eventually surpass the combined size of these three libraries.
The AI support
Zod is well-supported by AIs like ChatGPT. If you ask any question about it, you’ll often receive a ready-to-use schema right out of the box. Frequently, I simply provide the AI with field names, and I get back a solid boilerplate. This serves as a great starting point and significantly boosts productivity. Look how it works:
Life Is Insanely Easy
Overview Of Zod Features
There are a ton of possibilities with Zod. Here's a summary:
- Validate arrays, objects, and any data type.
- Determine TypeScript type definitions via
z.infer
. - Use unions and enums.
- Modify the validation parse mechanism to either throw an error or return a flag.
- Conduct validation in both
sync
andasync
manners. - Create your own validators.
- Combine validators via inheritance or direct pick mechanisms to avoid duplication in schemas.
- Easy to create form validations after installing the adapters mentioned above.
Summary
I remember when Zod first appeared in the community. Initially, I was skeptical, but after incorporating it into several apps, my perspective completely changed. Now, the code I write is simpler, and I avoid the naive approach of blindly trusting the backend.
With Zod, I have schemas that determine what the backend returns, and if it returns an invalid shape, I know immediately. Additionally, I've reduced my workload because I no longer need to duplicate type contracts and their implementations. It automatically gives me a type with z.infer
.
I'm not saying it's mandatory, but you should definitely try it and consider using it in your projects if you appreciate making your developer life easier.