Node.js Backend Fundamentals with Express · Build an API with Express

Validating Request Bodies with Zod

Validate and coerce incoming JSON with Zod schemas, return useful 400 errors, and never trust req.body.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A POST /orders endpoint whose body is validated with a Zod schema: types, ranges and required fields are checked, unknown fields are stripped, and clients get a 400 with field-level errors.

The idea

  • req.body is untrusted input. Validate shape, types and ranges before using it.
  • Zod schemas describe the expected data; schema.safeParse(value) returns either the parsed (typed, cleaned) data or detailed errors.
  • A small validate(schema) middleware keeps handlers clean: it replaces req.body with the parsed result or responds with 400.
  • Return errors as data the client can display: which field, what's wrong.

The code

A validation middleware with Zod
import express from "express";
import { z } from "zod";

const OrderInput = z.object({
  productId: z.number().int().positive(),
  quantity: z.number().int().min(1).max(100),
  email: z.email(),
  note: z.string().max(200).optional(),
});

const validate = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({
      error: "invalid request body",
      fields: result.error.issues.map((i) => ({ field: i.path.join("."), message: i.message })),
    });
  }
  req.body = result.data;           // parsed and stripped of unknown keys
  next();
};

const app = express();
app.use(express.json());
app.post("/orders", validate(OrderInput), (req, res) => res.status(201).json({ received: req.body }));

const server = app.listen(0);
const post = async (body) => {
  const r = await fetch(`http://127.0.0.1:${server.address().port}/orders`, {
    method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
  console.log(r.status, JSON.stringify(await r.json()));
};
await post({ productId: 7, quantity: 2, email: "[email protected]", isAdmin: true });
await post({ productId: "7", quantity: 0, email: "not-an-email" });
server.close();

Output

201 {"received":{"productId":7,"quantity":2,"email":"[email protected]"}}
400 {"error":"invalid request body","fields":[{"field":"productId","message":"Invalid input: expected number, received string"},{"field":"quantity","message":"Too small: expected number to be >=1"},{"field":"email","message":"Invalid email address"}]}

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Validating only types, not ranges (quantity: -5 or 1e9).
  • Passing req.body straight into a database query or object spread, letting clients set fields like isAdmin.
  • Returning a generic "bad request" with no detail, so clients can't fix their input.

Key points

  • Treat every request body, query and header as untrusted.
  • Describe input with a schema; validate, coerce and strip unknown fields.
  • Respond 400 with field-level errors.

Further reading: Zod documentation · OWASP Input Validation Cheat Sheet

Practice questions

Why replace req.body with the parsed result of a schema?

Answer

The parsed data has the validated types and no unknown keys, so later code can't accidentally use extra fields like isAdmin that a client added.

What should a validation error response contain?

Answer

A 400 status and a machine-readable list of field paths and messages so the client can show or fix the problems.

What is mass assignment?

Answer

Copying all client-supplied fields into a model or query, letting clients set fields they shouldn't (such as roles or prices); prevent it by whitelisting fields with a schema.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?