NodeJSRequest Validation

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

req.body

Missing/wrong-type fields, mass assignment

Route params

req.params

Non-numeric ids, injection

Query string

req.query

Bad pagination, unbounded limits

Headers

req.get(...)

Spoofed content type, missing auth

Never trust the client — validate types AND constraints
Clients (including malicious ones) send anything: missing fields, wrong types, gigantic strings, extra properties. Check both *type* (is `age` a number?) and *constraints* (is it 0–150?). Type-only checks miss business rules; constraint-only checks crash on the wrong type. Validate both, and reject early with a clear [422](/nodejs/api-error-responses) before any logic runs.
Manual validation — fine for one or two fields

JS
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
})
Manual checks don't scale — reach for a schema library
Hand-written checks are readable for a couple of fields but become a sprawling, error-prone mess for real payloads (nested objects, arrays, conditionals). A **schema validation library** declares the expected shape once and validates + coerces in one call. The popular choices: `zod`, `joi`, `yup`, and `express-validator`.
Schema validation with Zod

JS
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)
})
`safeParse` returns a result; `parse` throws
`safeParse` returns `{ success, data | error }` so you branch without `try/catch`. `parse` throws a `ZodError` on failure — convenient if you let your [error middleware](/nodejs/error-middleware) translate it to a 422. Either way you get back **typed, sanitized** data with defaults filled in — not the raw `req.body`. Use `result.data` downstream, never the original input.
A reusable validation middleware

JS
// 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)
Validate in middleware so handlers stay pure
Hoisting validation into a [custom middleware](/nodejs/custom-middleware) keeps handlers focused on business logic — by the time they run, `req.body` is already validated and coerced. The factory pattern lets one `validate(schema, part)` cover body, query, and params. This is essentially what `express-validator` does, with more wiring.
Guard against mass assignment

JS
// 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 }
Mass assignment: never spread `req.body` into a record
Copying the whole body into a database record (`{ ...req.body }`) lets a client inject fields you never intended — `isAdmin: true`, `balance: 9999`, someone else's `userId`. This is the **mass assignment** vulnerability. Defend by validating against a strict schema that only permits known fields (Zod strips unknown keys by default with `.strict()` it even rejects them), and explicitly pick the fields you persist. Never trust the body to contain only "reasonable" keys.
Where validation fits in the pipeline
  • Body parser (express.json()) → populates req.body.

  • Validation middleware → checks/coerces, or responds 422.

  • Auth/permission middleware → who can do this?

  • Handler → runs only on clean, authorized input.

Validation is not authorization — you need both
Validation answers "is this data well-formed?"; authorization answers "is this user allowed to do this?". A perfectly valid request can still be forbidden. Run both, in that order, before the handler. And remember: client-side validation is for UX only — it's trivially bypassed, so the server must re-validate everything regardless.
Next
Turn validation failures and errors into a consistent contract: [Consistent Error Responses](/nodejs/api-error-responses).