DEV Community

Cover image for Make Your TypeScript API Responses Safer with Zod (in seconds)
Eduardo Calzone
Eduardo Calzone

Posted on

Make Your TypeScript API Responses Safer with Zod (in seconds)

One of the most common problems we run into when trying to type our API responses—besides it being kind of tedious—is that we have zero control over when the API might change. And when it does... boom 💥 — your app can crash without warning.

The solution? Use the Zod library.

Zod lets you check your API responses at runtime, so you can make sure you're getting exactly what you expect—with the right types. No more silent type mismatches or weird crashes because the backend changed something without telling you.

🚀 Let’s See It in Action

Let’s say you’re calling a public API, like the Rick and Morty API, and you want to safely handle its response.

🔹 First, we hit the endpoint to get all the characters. 🎯

Characters endpoint

🔸 Next, we head over to Quicktype
Quicktype is a handy tool that turns raw JSON into ready-to-use TypeScript types. Super useful when working with API responses. ⚙️📦

There, we paste the JSON response from the characters endpoint into the left-hand panel.
Then, we select TypeScript with Zod on the right side to generate the corresponding schema. ⚡

Quicktype

🧪 Then, we drop the schema into our app and run a quick test to make sure everything works.

import * as z from "zod";


export const GenderSchema = z.enum([
    "Female",
    "Male",
    "unknown",
]);
export type Gender = z.infer<typeof GenderSchema>;


export const SpeciesSchema = z.enum([
    "Alien",
    "Human",
]);
export type Species = z.infer<typeof SpeciesSchema>;


export const StatusSchema = z.enum([
    "Alive",
    "Dead",
    "unknown",
]);
export type Status = z.infer<typeof StatusSchema>;

export const InfoSchema = z.object({
    "count": z.number(),
    "pages": z.number(),
    "next": z.string(),
    "prev": z.null(),
});
export type Info = z.infer<typeof InfoSchema>;

export const LocationSchema = z.object({
    "name": z.string(),
    "url": z.string(),
});
export type Location = z.infer<typeof LocationSchema>;

export const ResultSchema = z.object({
    "id": z.number(),
    "name": z.string(),
    "status": StatusSchema,
    "species": SpeciesSchema,
    "type": z.string(),
    "gender": GenderSchema,
    "origin": LocationSchema,
    "location": LocationSchema,
    "image": z.string(),
    "episode": z.array(z.string()),
    "url": z.string().url(),
    "created": z.coerce.date(),
});
export type Result = z.infer<typeof ResultSchema>;

export const RickAndMortyTypesSchema = z.object({
    "info": InfoSchema,
    "results": z.array(ResultSchema),
});
export type RickAndMortyTypes = z.infer<typeof RickAndMortyTypesSchema>;


async function fetchCharacters() {
  try {
    const res = await fetch("https://rickandmortyapi.com/api/character");

    if (!res.ok) {
      throw new Error(`HTTP error: ${res.status}`);
    }

    const json = await res.json();

    const parsed = RickAndMortyTypesSchema.safeParse(json);

    if (!parsed.success) {
      console.error("❌ Invalid API response:", parsed.error.format());
      return;
    }

    const data: RickAndMortyTypes = parsed.data;
    console.log("✅ Valid data:", data);
  } catch (err) {
    console.error("❌ Fetch failed:", err);
  }
}

fetchCharacters();
Enter fullscreen mode Exit fullscreen mode

🚀 Now, the final test!
Let’s call the endpoint and validate the response using our new Zod schema. If everything works, we’ll get a fully typed, safe result — and if not, Zod will let us know! ✅⚠️

Zod


⚠️ We also introduce a deliberate error by setting name as a number instead of a string, so we can see Zod in action catching the issue. 🛑👀

Zod validation

✅ We observe the expected behavior, confirming that Zod validates our data correctly. 🎯


Source: Typescript Zod Validator

Happy coding! 🥳

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more