← back to blog

Tutorials

Building a Type-Safe API Layer in React with TypeScript

Stop sprinkling `any` across your fetch calls. Here's the small, boring pattern I use on every production app to keep server data honest.

Edwine Nyakundi

Edwine Nyakundi

Full stack engineer · Sep 12, 2026 · 9 min read

Every React codebase I've inherited has the same soft spot: the boundary where network data enters the app. Types are asserted, not verified, and the first schema change from the backend turns into a runtime crash in production.

Validate at the boundary

The fix is unglamorous. Parse the response with a schema, once, at the edge. Everything downstream can then trust its types because they were earned, not declared.

ts
import { z } from class="tok-string">"zod";

const User = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.coerce.date(),
});

export type User = z.infer<typeof User>;

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

One client, many resources

Wrap the repetitive parts — base URL, headers, error shape — in a tiny helper. Not a framework, just a function you can read in ten seconds.

ts
async function request<T>(path: string, schema: z.ZodType<T>, init?: RequestInit) {
  const res = await fetch(path, {
    ...init,
    headers: { class="tok-string">"content-type": class="tok-string">"application/json", ...init?.headers },
  });

  if (!res.ok) {
    const body = await res.text();
    throw new ApiError(res.status, body);
  }

  return schema.parse(await res.json());
}

Where this pays off

  • Backend renames a field — you get one loud error in one place instead of ten silent undefineds.
  • Dates arrive as strings but leave as Date objects, so no more `new Date(x)` scattered through components.
  • Your query hooks infer their return types for free.
Types you didn't verify are just comments with better syntax highlighting.

It costs maybe twenty extra lines per resource. I've never regretted writing them.

#TypeScript#React#Architecture

Related posts

Newsletter

Get new posts in your inbox

Deep dives on TypeScript, Postgres, and engineering careers — roughly twice a month. No courses to sell, no spam.