Back to Deep Dives
TypeScript
Madhusudhan Kakurala 2 min read

Building Type-Safe APIs with TypeScript and Zod

Learn how to create robust, type-safe APIs using TypeScript and Zod for runtime validation that keeps your frontend and backend in sync.

Type safety is one of TypeScript’s greatest strengths, but it only helps at compile time. What about runtime? That’s where Zod comes in.

The Problem

Consider a typical API response:

interface User {
  id: number;
  name: string;
  email: string;
}

const response = await fetch('/api/user/1');
const user: User = await response.json(); // Trust issues!

TypeScript trusts that the response matches our interface, but what if it doesn’t?

Enter Zod

Zod lets us define schemas that validate at runtime while inferring TypeScript types:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

Validating API Responses

Now we can safely parse responses:

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/user/${id}`);
  const data = await response.json();
  return UserSchema.parse(data); // Throws if invalid
}

Safe Parsing

For graceful error handling:

const result = UserSchema.safeParse(data);

if (result.success) {
  console.log(result.data); // Typed as User
} else {
  console.error(result.error.issues);
}

Best Practices

  1. Define schemas once - Share between frontend and backend
  2. Use strict mode - Catch extra properties with .strict()
  3. Transform data - Use .transform() for data manipulation
  4. Provide defaults - Use .default() for optional fields

Conclusion

Combining TypeScript with Zod gives you the best of both worlds: compile-time type checking and runtime validation. Your APIs become truly type-safe.