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.