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".