Request Validation (Zod & express-validator)

Request validation is the practice of checking and cleaning incoming data on the server before you trust it, rejecting anything malformed with a clear 400 response.

Learn Request Validation (Zod & express-validator) in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice…

Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll learn why server-side validation is non-negotiable, define schemas with Zod using parse and safeParse , add per-route rules with express-validator , return helpful 400 errors, and sanitize input.

What You'll Learn in This Lesson

1️⃣ Schemas with Zod (parse)

A schema is a description of the data you expect. With Zod you build one with z.object(...) and chained refinements like .email() , .min(18) , or .int() . Calling .parse(data) returns the clean, typed value — or throws a ZodError that lists every problem at once.

2️⃣ Handling Failures with safeParse

In a request handler you usually don't want exceptions. schema.safeParse(data) never throws: it returns {' '} or {' '} . Check result.success , and on failure call error.flatten().fieldErrors to get a tidy {' '} object for your 400 response.

3️⃣ express-validator Middleware

If you're already in Express, express-validator attaches rules as middleware right on the route. Each body('field') describes a check ( .isEmail() , .isLength({' '}) ) with a custom .withMessage() . In the handler, validationResult(req) gathers any failures so you can return a 400.

Validation isn't only about rejecting — it's also about cleaning . Zod can trim whitespace, lowercase strings, and coerce a form's string into a real number, all while validating:

Your turn. Fill in the three blanks marked ___ to finish the product schema, then run it and compare with the expected output.

No blanks this time — build the schema and validator yourself from the brief. Trim strings, cap the body length, and coerce the rating to a 1–5 integer. Run it and check your output against the example in the comments.

📋 Quick Reference — Validation

Practice quiz

Why is server-side validation non-negotiable?

  • It makes forms look nicer
  • Client-side checks can be bypassed by calling the API directly
  • It is faster than the browser
  • Browsers require it

Answer: Client-side checks can be bypassed by calling the API directly. Anyone can skip the form with curl or a script, so the server is the only trustworthy gate.

What does Zod's schema.parse(data) do on invalid input?

  • Returns { success: false }
  • Returns null
  • Throws a ZodError
  • Logs a warning and continues

Answer: Throws a ZodError. parse() returns the typed data on success or throws a ZodError on failure.

How does safeParse() report a failure?

  • It returns { success: false, error }
  • It throws an exception
  • It returns undefined
  • It exits the process

Answer: It returns { success: false, error }. safeParse never throws; it returns { success, data } or { success, error }.

What does error.flatten().fieldErrors give you?

  • A stack trace
  • The raw request body
  • A list of valid fields

flatten().fieldErrors produces a tidy per-field error object for responses.

In express-validator, how do you define a rule on a field?

  • body('email').isEmail()
  • validate('email')
  • check.email()
  • req.body.email.valid()

Answer: body('email').isEmail(). Each body('field') call attaches a validation rule as middleware.

How do you read express-validator failures in the handler?

  • req.errors
  • validationResult(req)
  • res.errors()
  • body.errors

Answer: validationResult(req). validationResult(req) collects whatever the rules flagged.

Form fields and query strings arrive as what type?

  • Numbers
  • Strings
  • Booleans
  • Objects

Answer: Strings. They are always strings, so '36' must be coerced before numeric comparison.

Which Zod helper turns the string '36' into the number 36?

  • z.number()
  • z.string().toNumber()
  • z.coerce.number()
  • z.cast.int()

Answer: z.coerce.number(). z.coerce.number() coerces the incoming string into a real number.

What HTTP status code fits an invalid request body?

  • 200 OK
  • 500 Internal Server Error
  • 301 Moved
  • 400 Bad Request

Answer: 400 Bad Request. Return 400 Bad Request with a list of which fields failed and why.

A ZodError reports how many of the problems found?

  • Every problem at once
  • Only the first one
  • Only the last one
  • None, just a boolean

Answer: Every problem at once. A ZodError lists every issue in err.issues so the user can fix the whole form.