← Recipes

阅读中文版本

Validate an API Payload with Zod at the Edge

Define a zod schema for the request body, parse the incoming payload before the handler runs, and return a 400 with a field-level error map on failure.

~25 minzodvalidationapi

Task

Reject invalid API requests at the edge with field-level errors so the handler only sees typed, valid data.

When to use this

A handler assumes email is a string and role is an enum, but the client sends a number and a typo. The handler crashes mid-transaction. The team wants validation before the handler, with errors the client can act on.

Prerequisites

  • Node.js 20 LTS with zod 3 installed
  • A request body parser that yields an unknown value
  • An error response shape agreed with the frontend

Steps

1. Define the schema with a field-level error message for each constraint.

const schema = z.object({
  email: z.string().email("email must be valid"),
  role: z.enum(["admin", "user"], { errorMap: () => ({ message: "role must be admin or user" }) }),
});

Expected output: A schema that rejects a non-email string and an unknown role with the custom messages.

2. Parse the body in middleware and return 400 with the flattened error map on failure.

const result = schema.safeParse(req.body);
if (!result.success) {
  return res.status(400).json({ errors: result.error.flatten() });
}
req.body = result.data;

Expected output: An invalid request gets a 400 with one error per field; a valid request reaches the handler with typed data.

3. Derive a static type from the schema so the handler has compile-time guarantees.

type Input = z.infer<typeof schema>;
function handler(body: Input) { /* body.email is string, body.role is enum */ }

Expected output: The handler parameter is typed, and a typo in a field access is a compile error, not a runtime crash.

Troubleshooting

Problem

The error map shape does not match what the frontend expects

Solution

Use flatten or format to control the shape, and document the contract in the OpenAPI spec so the frontend codes against it.

Problem

A union type produces a confusing error

Solution

Add an errorMap to the union that names each option, so the client sees which branch failed.

Limitations

  • Zod validates shape, not authorization; a valid payload can still be forbidden for the caller.
  • A schema that uses refinements runs them on every parse, which can be expensive on large arrays.

Validate before you handle

Parse the body in middleware, not in the handler. On failure, return a 400 with a field-level error map. On success, replace the body with the parsed value so the handler sees typed data and cannot crash on a missing field.

const result = schema.safeParse(req.body);
if (!result.success) {
  return res.status(400).json({ errors: result.error.flatten() });
}
req.body = result.data;

Derive the type

Use z.infer to get a TypeScript type from the schema. The handler parameter is typed, so a typo in a field access is a compile error. The schema is the single source of truth for both runtime validation and compile-time types.

type Input = z.infer<typeof schema>;
function handler(body: Input) { /* body.email is string */ }

Expected outcome

Invalid requests get a 400 with one message per field, valid requests reach the handler with typed data, and the handler never crashes on a missing or malformed field. The schema drives both the validation and the types.