IQ
PayloadIQ
← PayloadIQ Guides

From API Response to a Typed Client

A repeatable path from a raw response to a small, typed fetch client with validation, a mock, and an example — without hand-writing each layer.

Most teams call an endpoint the same way every time: fetch, res.json(), then read fields and hope. It works until the response shifts. A typed client turns that ad-hoc call into a small, named function with types, validation, and a mock — so the endpoint behaves like a proper module instead of a string you pass around.

The path has five steps, and each one earns its place.

1. Types from the response

Start with a real payload and derive the interface:

export interface User {
  id: number;
  email: string;
  role: "admin" | "editor" | "viewer";
  lastLogin: string | null;
}

2. A Zod schema

Types describe the shape; the schema enforces it at runtime. Deriving the type from the schema keeps them in lockstep:

import { z } from "zod";

export const UserSchema = z.object({
  id: z.number().int(),
  email: z.string().email(),
  role: z.enum(["admin", "editor", "viewer"]),
  lastLogin: z.string().datetime().nullable(),
});
export type User = z.infer<typeof UserSchema>;

3. A client that validates

The client function wraps fetch, parses with the schema, and returns a typed value. Validation lives here, at the boundary:

export async function getUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`getUser failed: ${res.status}`);
  return UserSchema.parse(await res.json());
}

Every caller now gets a User that has actually been checked — not a hopeful cast.

4. A mock for tests

A mock that satisfies the same schema lets tests run without the network and guarantees your fixture stays valid as the schema evolves:

export const mockUser: User = {
  id: 1,
  email: "test@example.com",
  role: "admin",
  lastLogin: null,
};

5. An example call

Finally, a one-line usage example documents the happy path for the next person: const user = await getUser(1042);. Small, but it saves a trip to the source.

When to use it

Build a typed client for any endpoint you call from more than one place, or where the response shape genuinely matters to the UI. For a one-off script or a throwaway prototype, it's overkill — a plain fetch is fine. The investment pays off when the same data flows through several components and you want one place to change when the API does.

Common mistakes

  • Returning res.json() untyped. The value is any, and any poisons every line it touches.
  • Hand-duplicating the shape across files. Types in one file, schema in another, mock in a third — three things to update, two of which you'll forget.
  • No mock. If tests hit the real network, they're slow and flaky and break when the backend is down.
  • Swallowing validation errors. Catching the parse error and returning null hides the contract break you wanted to catch.

Generating these five layers by hand for every endpoint is the tedious part. The PayloadIQ Typed Client produces the types, schema, validating client, and mock together from one pasted response, so you can review and adjust rather than type boilerplate.

Open Typed Client

Related guides

JSON to TypeScriptJSON to ZodSchema Quality Check