typescript 48 lines · 8 steps

A type-safe fetch wrapper with Zod

A generic apiFetch validates HTTP responses against a Zod schema so the return type is inferred and checked at runtime.

Explained by highlit
1import { z, type ZodType } from "zod";
2 
3export class HttpError extends Error {
4 constructor(
5 public readonly status: number,
6 public readonly body: unknown,
7 ) {
8 super(`Request failed with status ${status}`);
9 this.name = "HttpError";
10 }
11}
12 
13interface RequestOptions<TSchema extends ZodType> {
14 schema: TSchema;
15 method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
16 body?: unknown;
17 headers?: Record<string, string>;
18 signal?: AbortSignal;
19}
20 
21export async function apiFetch<TSchema extends ZodType>(
22 url: string,
23 { schema, method = "GET", body, headers, signal }: RequestOptions<TSchema>,
24): Promise<z.infer<TSchema>> {
25 const response = await fetch(url, {
26 method,
27 signal,
28 headers: {
29 Accept: "application/json",
30 ...(body !== undefined && { "Content-Type": "application/json" }),
31 ...headers,
32 },
33 body: body !== undefined ? JSON.stringify(body) : undefined,
34 });
35 
36 const raw = response.status === 204 ? null : await response.json();
37 
38 if (!response.ok) {
39 throw new HttpError(response.status, raw);
40 }
41 
42 const parsed = schema.safeParse(raw);
43 if (!parsed.success) {
44 throw new Error(`Response validation failed: ${parsed.error.message}`);
45 }
46 
47 return parsed.data;
48}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Threading a Zod schema through a generic lets one function guarantee both compile-time types and runtime shape.
  2. 2A custom error class carrying status and body gives callers structured failure data instead of a bare message.
  3. 3Validating at the boundary means untyped JSON never leaks into the rest of your code unchecked.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A type-safe fetch wrapper with Zod — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code