Request Validation & Parsing

core20 min

In one line

Validate at the boundary by parsing into a typed value, so that everything downstream is guaranteed correct by construction rather than by everyone remembering to check.

What it is

A request body is a string of bytes from a stranger. Parsing is the step that turns it into a value your domain code can trust, and the useful framing is Alexis King's: a validator returns a boolean and throws the knowledge away, a parser returns a narrower type. parse(body) giving you a CreateOrder means no function downstream needs to ask whether quantity is a number — the type says it is, because the only way to get one was through the schema.

In a TypeScript service this is where Zod (or Valibot, or ArkType) earns its place: one schema per endpoint, schema.parse(await req.json()) as the first line of the handler, and the inferred type flows through the rest. It closes the gap TypeScript can't — types vanish at runtime, so req.body as CreateOrder is a lie the compiler will happily believe.

Validate all four inputs, not just the body: path parameters, query strings, headers, and the body. Query strings are the most commonly missed, and they arrive as strings, so coercion ("2"2, "true"true) belongs in the schema rather than scattered through the handler.

Three rules that decide whether validation actually protects you. Allowlist, don't blocklist — enumerate what is permitted, because you cannot enumerate every bad input. Reject unknown fields on writes (Zod's .strict()), or a client typo silently becomes a no-op and an attacker can attempt mass assignment on fields like role or isAdmin. And bound everything: max string length, max array length, max body size, so one request can't allocate a gigabyte.

Validation is not authorization and not sanitisation. A well-formed request from someone with no permission is still a 403, and a valid string is still dangerous when concatenated into SQL or HTML. The schema proves shape, nothing more.

Why it matters

Take-home reviewers explicitly score error and edge-case handling, and an unvalidated req.body is the single most visible tell in a submission. Operationally it is the cheapest defence you have: mass assignment, injection, and half your 500s from undefined property access all die at a strict schema on the boundary.

Key points

  • Parse into a typed value at the boundary; don't validate-and-forget, because the type is what protects the rest of the service.
  • TypeScript types are erased at runtime — a cast on request data is an assertion, not a check.
  • Validate path params, query, headers and body; query values arrive as strings and need explicit coercion.
  • Allowlist permitted fields and reject unknown ones, or mass assignment quietly becomes a privilege escalation path.
  • Bound sizes — string length, array length, request body — before the parse allocates on your behalf.
  • Validation answers "is this well-formed", never "is this allowed" or "is this safe to interpolate".