Request Validation
Every byte a client sends is untrusted. Validation is the gate that ensures the data entering your handlers has the right shape, types, and constraints — before it reaches your database or business logic. Skipping it leads to corrupt data, crashes (.trim() on undefined), and security holes (injection, mass assignment). This page covers what to validate, where, and how — using a schema library (Zod) to keep it declarative.
Validate every input surface
Source | Express | Typical risks |
|---|---|---|
Request body |
| Missing/wrong-type fields, mass assignment |
Route params |
| Non-numeric ids, injection |
Query string |
| Bad pagination, unbounded limits |
Headers |
| Spoofed content type, missing auth |
Manual validation — fine for one or two fields
app.post('/users', (req, res) => {
const { email, age } = req.body
const errors = []
if (typeof email !== 'string' || !email.includes('@')) {
errors.push({ field: 'email', message: 'valid email required' })
}
if (age !== undefined && (typeof age !== 'number' || age < 0)) {
errors.push({ field: 'age', message: 'age must be a positive number' })
}
if (errors.length) return res.status(422).json({ errors })
// ...safe to proceed
})Schema validation with Zod
import { z } from 'zod'
const CreateUser = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['user', 'admin']).default('user'),
})
app.post('/users', (req, res) => {
const result = CreateUser.safeParse(req.body)
if (!result.success) {
return res.status(422).json({ errors: result.error.issues })
}
const data = result.data // typed, coerced, defaults applied
createUser(data)
res.status(201).json(data)
})A reusable validation middleware
// Factory: validate a given part of the request against a schema.
const validate = (schema, part = 'body') => (req, res, next) => {
const result = schema.safeParse(req[part])
if (!result.success) {
return res.status(422).json({
error: 'Validation failed',
details: result.error.issues.map((i) => ({
field: i.path.join('.'),
message: i.message,
})),
})
}
req[part] = result.data // replace with the cleaned, typed value
next()
}
app.post('/users', validate(CreateUser), createUserHandler)
app.get('/users', validate(ListQuery, 'query'), listUsersHandler)Guard against mass assignment
// DANGER: spreading raw body lets a client set fields they shouldn't:
const user = { ...req.body } // attacker sends { "role": "admin" } 🚩
// SAFE: a strict schema strips unknown keys; pick only what you allow:
const data = CreateUser.parse(req.body) // 'role' only if schema permits it
const user = { email: data.email, name: data.name }Where validation fits in the pipeline
Body parser (
express.json()) → populatesreq.body.Validation middleware → checks/coerces, or responds
422.Auth/permission middleware → who can do this?
Handler → runs only on clean, authorized input.