zod boundaries

why

  • every untrusted input — HTTP payload, provider response, webhook, file, environment — is a shape assumption until parsed.
  • unparsed assumption failing = runtime crash deep inside business logic, not clean rejection at the edge.
  • named schemas + inferred types keep validated shape and consumer in one declaration — no drift between validation and use.

rule

implement boundary validation with named Zod schemas. preserve the inferred type; never rewrite it as a parallel interface.

boundaries

  • HTTP request, query, parameter, and response payloads.
  • provider responses, webhooks, files, environment configuration, and sockets.
  • persisted data read outside a trusted migration boundary.

boundary workflow

  1. accept unknown at the transport, provider, file, or environment edge.
  2. parse with one named schema whose name identifies the consumer shape.
  3. return the inferred type from the parser; no parallel type assertion or manually synchronized interface.
  4. map parsed external data to a domain fact only when source and destination have different stable meanings.
  5. expose typed diagnostic failure at the same boundary when parsing fails.

decide schema ownership

valueschema ownerreason
HTTP request/responseroute or shared contract moduleit is executable transport behavior and API documentation input.
provider/webhook payloadadapter boundaryprovider vocabulary must not become a domain contract.
environment settingconfiguration boundaryprocess startup should fail before partial runtime behavior.
persisted record read through a trusted schema/migrationpersistence boundarystorage changes need one auditable interpretation.
local already-typed computationno redundant schemavalidation adds no authority when the type is produced inside the same trusted boundary.

failure modes prevented

shortcutresult
cast unknown as Domainmalformed data reaches business logic and fails without an input boundary.
validate only a field used todaylater logic consumes an unchecked part of the same payload.
duplicate a schema as an interfacetype and runtime validation drift independently.
parse inside several consumerseach caller invents a slightly different acceptance rule.

verification

  • run valid and malformed representative values through the schema at its real boundary.
  • confirm the inferred type is the only transport type consumers use.
  • confirm the diagnostic preserves a safe remediation category.
  • confirm provider fields do not escape without an explicit mapper.

do not

  • cast unknown into a domain type.
  • duplicate a Zod schema as handwritten transport types.
  • validate only after business logic already consumed the value.

good and bad

bad

// route handler accepts requestBody and asserts without validation
const payload = requestBody as CreateWorkflowRunInput;
return createWorkflowRun(payload);
  • assertion accepts malformed external data without a diagnosable boundary.
  • client sends { name: null } instead of { name: "My Workflow" }: type checker thinks it is safe, domain logic crashes on null propagation or produces a broken workflow.
  • debugging traces backward from the crash, but the entry boundary accepted the bad shape silently.

good

// route handler validates at the boundary
const payload = CreateWorkflowRunInputSchema.parse(requestBody);
return createWorkflowRun(payload);
// if requestBody is { name: null }, parse fails immediately with a typed diagnostic
// "CreateWorkflowRun: name must be a string"
  • parser accepts unknown, establishes the inferred type, fails where the untrusted value entered the system.
  • malformed input caught before it reaches domain logic.
  • later developers reading the domain function can trust its inputs are validated.

search pages

go to any page