Validate API Responses at the Boundary in TypeScript

Treat incoming JSON as unknown, then check its shape before handing it to the rest of your application.

A woodworking jig can check whether a block fits a particular opening. A block that fits passes through. An ill-fitting piece stays on the other side, where you can inspect it before it reaches the next step.

An API response needs a similar checkpoint. A TypeScript type describes what your code expects, but the server can still send a missing field, a `null`, or a number where you expected a string. A runtime check lets you catch those mismatches where the data enters your code.

Start with `unknown`

Assign the result of `response.json()` to a variable typed as `unknown`. That makes TypeScript require a check before you read fields from it. A type assertion such as `as Product` would let you access those fields immediately without checking what the server sent.

type Product = {
  id: string;
  name: string;
};

function isProduct(value: unknown): value is Product {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "string" &&
    "name" in value &&
    typeof value.name === "string"
  );
}

async function fetchProduct(url: string): Promise<Product> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Product request failed (${response.status})`);
  }

  const data: unknown = await response.json();
  if (!isProduct(data)) {
    throw new Error("Expected a product with a string id and name");
  }
  return data;
}

The `value is Product` return type is a *type predicate*. When the function returns `true`, TypeScript narrows `data` to `Product`, so callers can use `product.name` as a string. A payload such as `{ "id": 42, "name": "Clamp" }` parses as JSON but fails the guard because its `id` is a number. A malformed JSON body fails earlier, during `response.json()`.

Check what the next step needs

This guard accepts extra fields and empty strings. If the application requires a nonempty ID, add that check to the guard after confirming `id` is a string. The guard is like the jig opening. Its dimensions should match what the next step can work with.

Back to Journal