Skip to content
AB

TypeScript Types Disappear at Runtime: Validate API Boundaries with Zod

Static types cannot protect a running API from malformed input. Learn how Zod turns untrusted payloads into safe application data.

Ashish BhakhandPublished

TypeScript made my backend code easier to change, but it did not make incoming data safe. A client can still omit a field, send a string where I expect a number, or add properties I never intended to accept. The type annotation disappears when JavaScript runs.

Code
type CreateProfileInput = {
  name: string;
  age: number;
};

const input = JSON.parse(request.body) as CreateProfileInput;

The as keyword performs no validation. It only asks the compiler to trust me. If age is "twenty", the lie travels into the application.

Treat the request as unknown#

Every value outside the process begins as unknown. I use Zod to prove its shape before business logic sees it:

Code
import { z } from "zod";

const createProfileSchema = z
  .object({
    name: z.string().trim().min(2).max(80),
    age: z.coerce.number().int().min(13).max(120),
  })
  .strict();

type CreateProfileInput = z.infer<typeof createProfileSchema>;

const result = createProfileSchema.safeParse(JSON.parse(request.body));

Now the schema is the source of truth and the TypeScript type is inferred from it. .strict() also rejects unexpected keys, which catches misspelled fields and prevents accidental mass assignment.

Validation and business rules are different#

Zod should answer questions about the boundary:

  • Is the value present and of the right shape?
  • Is a date syntactically valid?
  • Is a page size within a safe range?

The application should answer questions that require business knowledge:

  • Is this email already registered?
  • May this member edit this organization?
  • Is the booking window still open?

Putting database checks inside a schema couples parsing to infrastructure and makes both harder to test.

Return useful errors#

A validation failure should become a stable API response rather than a raw Zod error:

Code
if (!result.success) {
  return {
    statusCode: 400,
    body: JSON.stringify({
      code: "VALIDATION_ERROR",
      message: "Some fields are invalid.",
      fields: result.error.flatten().fieldErrors,
    }),
  };
}

await createProfile(result.data);

Keep internal diagnostics in logs, but give clients a predictable code and field-level feedback. Do not expose stack traces or implementation details.

Validate environment variables, queue messages, webhook payloads, database documents from older deployments, and API requests. A typed function is safe only after untrusted data has crossed a runtime boundary. TypeScript describes the contract; Zod checks that reality follows it.

← Back to all writing