Validating Runtime Data with Zod and TypeScript
TypeScript checks your code, not your data. A JSON response, a form submission or an environment variable can be anything at runtime. Zod validates data against a schema and gives you the TypeScript type from that same schema.
Define once, get both
import { z } from 'zod';
const User = z.object({
id: z.number().int(),
name: z.string().min(1),
email: z.email(),
role: z.enum(['admin', 'member']),
createdAt: z.coerce.date(),
});
type User = z.infer<typeof User>; // the TypeScript type, no duplication
Validate API responses
async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return User.parse(await res.json()); // throws a ZodError if the shape is wrong
}
Handle errors without exceptions
const result = User.safeParse(body);
if (!result.success) {
return Response.json({ errors: z.flattenError(result.error).fieldErrors }, { status: 422 });
}
const user = result.data; // typed User
Validate environment variables at startup
const Env = z.object({
DATABASE_URL: z.url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'test', 'production']),
});
export const env = Env.parse(process.env); // crash early with a clear message
Useful patterns
.optional(),.nullable()and.default()for missing values..transform()to normalise data (trim, lowercase) during parsing..refine()for custom rules such as "end date after start date".User.partial()for update payloads,User.omit({ id: true })for create payloads.
The examples use the Zod 4 API. Zod 3 code like z.string().email() still works but the top-level forms such as z.email() are preferred.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.