NodeJSWhy Validate Input?

Why Validate Input?

Input validation is the discipline of checking that data entering your application is the shape, type, and range you expect — before you act on it. It is not optional polish: it's the front line of both correctness and security. Every crash from undefined, every injection attack, every corrupt database row traces back to data that wasn't validated. This page makes the case for validation, distinguishes it from sanitization and authorization, and frames the library choices (Joi, Zod, express-validator) that follow.

Where untrusted data enters

Source

Example

What goes wrong unvalidated

Request body

JSON, form fields

Missing/wrong-type fields, mass assignment

Route & query params

/users/:id, ?limit=

Non-numeric ids, unbounded queries

Headers & cookies

Authorization, prefs

Spoofed values

File uploads

Avatars, documents

Oversized files, disguised executables

External APIs

Third-party responses

Unexpected shapes break your code

Env & config

process.env

Missing vars surface as runtime crashes

ALL input is untrusted — including from your own frontend
A frequent rationalization: "my React app already validates this, so the server doesn't need to." Wrong. Client-side checks are a **UX convenience** and are trivially bypassed — anyone can hit your endpoint with `curl`, Postman, or a script. The server is the only place validation actually enforces anything. Validate every input at the boundary, no matter how trusted the caller seems.
What validation protects against
  • Crashesreq.body.name.trim() throws if name is undefined.

  • Corrupt data — a string where a number belongs poisons your store and every later read.

  • Injection — unvalidated input concatenated into SQL/NoSQL/shell commands.

  • Mass assignment — a client setting isAdmin: true by smuggling extra fields.

  • Resource abuse?limit=9999999 exhausting memory; unbounded uploads filling disk.

  • Logic errors — out-of-range values (negative quantity, age 999) violating business rules.

Validation vs sanitization vs authorization

Concern

Question it answers

Example

Validation

Is the data well-formed & in range?

Is email a valid email? Is age 0–150?

Sanitization

Can I clean/normalize it?

Trim whitespace, lowercase email, strip HTML

Authorization

Is this user allowed to do this?

Can this user edit that post?

These are three different jobs — you need all three
Validation rejects malformed input; sanitization normalizes acceptable input (trim, coerce, escape); authorization decides permission. A request can be perfectly valid yet forbidden, or valid yet need normalizing. Don't conflate them: a schema library handles validation (and often coercion), but **authorization is separate** logic that runs after you know the data is well-formed.
Fail fast, fail clearly

JS
// Reject bad input at the boundary with a clear, structured 422:
app.post('/users', (req, res, next) => {
  const result = validateUser(req.body)
  if (!result.ok) {
    return res.status(422).json({
      error: 'Validation failed',
      details: result.errors,    // field-level messages the client can use
    })
  }
  req.validated = result.data    // pass CLEANED data downstream, not req.body
  next()
})
Validate at the edge; trust the cleaned data inward
Do validation as early as possible — in middleware, before any business logic — and respond with a structured [422](/nodejs/api-error-responses) listing what failed. Crucially, pass the **validated/coerced** result downstream (`req.validated`), not the raw `req.body`. Once data is validated at the boundary, inner code can trust it, which keeps deep logic free of defensive `if (typeof x !== ...)` clutter.
Why a library beats hand-rolled checks
  • Declarative — describe the shape once; the library does the checking and coercion.

  • Composable — reuse and nest schemas (a User contains an Address).

  • Coercion & defaults — turn "42" into 42, fill optional fields.

  • Consistent errors — uniform, field-level messages out of the box.

  • TypeScript inference — (Zod) derive static types from the schema, one source of truth.

The three you'll meet next
Manual `if` checks are fine for one or two fields but collapse under real payloads. The ecosystem's main options: **[Joi](/nodejs/joi)** — mature, feature-rich, framework-agnostic; **[Zod](/nodejs/zod)** — TypeScript-first with type inference, now the default for many new projects; **[express-validator](/nodejs/express-validator)** — middleware-style, integrated tightly with Express request objects. The next pages cover each.
A note on environment & config validation

JS
// Validate env vars at startup so misconfig fails loudly, not at 3am:
const Env = z.object({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  NODE_ENV: z.enum(['development', 'production', 'test']),
})
export const env = Env.parse(process.env)   // throws immediately if invalid
Validate config at boot — a missing env var should crash on startup
Validation isn't only for request data. Parse `process.env` against a schema at startup so a missing `DATABASE_URL` or malformed `PORT` fails *immediately and clearly*, rather than surfacing as a cryptic `undefined` error deep in production traffic. Fail at boot, not at request time.
Next
A mature, expressive validation library: [Validation with Joi](/nodejs/joi).