Validate API Responses in TypeScript with Zod
Table of Contents
Validating API responses with Zod means defining a schema that matches the expected shape, then calling schema.safeParse() on the raw JSON. If the response matches, you get a fully-typed object. If not, you get structured error details — not a runtime crash.
This guide walks through the full pattern for validating GET and POST responses, handling errors, and generating schemas from real API output.
The Problem: Unvalidated API Responses
Most TypeScript codebases cast API responses like this:
const data = await res.json() as Product; // dangerous
This tells TypeScript to trust the data structure. It does NOT check anything. External APIs can:
- Return unexpected fields or missing required fields
- Return strings where you expect numbers
- Return null for fields your code assumes are always present
- Change their response format between API versions
Any of these cases causes bugs that are silent at compile time and crash at runtime. The fix is runtime validation with a schema.
Setting Up Zod for API Validation
Install Zod if you have not already:
npm install zod
Define a schema that matches the API response shape. If you have a real JSON response, paste it into the JSON to Zod converter to generate the base schema automatically, then customize it.
import { z } from 'zod';
const ProductSchema = z.object({
id: z.number(),
name: z.string(),
price: z.number(),
inStock: z.boolean(),
tags: z.array(z.string()),
category: z.object({
id: z.number(),
name: z.string()
})
});
type Product = z.infer<typeof ProductSchema>;
Sell Custom Apparel — We Handle Printing & Free Shipping
Validating GET Responses
Use safeParse for non-throwing validation:
async function getProduct(id: number): Promise<Product | null> {
const res = await fetch("/api/products/" + id);
if (!res.ok) {
throw new Error("Request failed: " + res.status);
}
const json = await res.json();
const result = ProductSchema.safeParse(json);
if (!result.success) {
console.error("Invalid API response:", result.error.issues);
return null;
}
return result.data; // typed as Product
}
The returned result.data is fully typed. TypeScript knows exactly what fields it has.
Validating List Responses
Wrap the schema in z.array() for list endpoints:
const ProductListSchema = z.array(ProductSchema);
async function getProducts(): Promise<Product[]> {
const res = await fetch("/api/products");
const json = await res.json();
return ProductListSchema.parse(json); // throws on invalid
}
For paginated responses where the array is nested:
const PaginatedSchema = z.object({
data: z.array(ProductSchema),
total: z.number(),
page: z.number(),
pageSize: z.number()
});
Validating POST Request Bodies
Use Zod to validate request inputs on the server side too:
const CreateProductSchema = z.object({
name: z.string().min(1).max(100),
price: z.number().positive(),
tags: z.array(z.string()).optional()
});
// Express handler
app.post('/api/products', (req, res) => {
const result = CreateProductSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.issues });
}
const product = result.data; // validated and typed
// ... create product
});
This pattern works in Express, Fastify, Next.js API routes, and any other Node.js HTTP framework.
Try It Free — No Signup Required
Runs 100% in your browser. No data is collected, stored, or sent anywhere.
Open Free JSON to Zod ConverterFrequently Asked Questions
How do I validate a fetch API response with Zod?
Call res.json() to get the data, then call YourSchema.safeParse(data). Check result.success to see if validation passed. If true, result.data is typed. If false, result.error.issues shows what failed.
Should I use parse() or safeParse() for API responses?
Use safeParse() for most cases — it returns a result object instead of throwing, making error handling cleaner. Use parse() only when you want the function to throw on invalid data and you handle exceptions upstream.
Can Zod validate nested API responses?
Yes. Nest z.object() inside z.object() for nested data. Arrays of objects use z.array(z.object({...})). The converter handles this automatically from your JSON sample.
How do I generate a Zod schema from a real API response?
Paste the JSON response into the free converter at /developer-tools/json-to-zod/. It generates the full schema automatically. Then add any custom constraints (.email(), .min(), .optional()) that the sample alone cannot infer.

