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 |
| Non-numeric ids, unbounded queries |
Headers & cookies |
| Spoofed values |
File uploads | Avatars, documents | Oversized files, disguised executables |
External APIs | Third-party responses | Unexpected shapes break your code |
Env & config |
| Missing vars surface as runtime crashes |
What validation protects against
Crashes —
req.body.name.trim()throws ifnameisundefined.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: trueby smuggling extra fields.Resource abuse —
?limit=9999999exhausting 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 |
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? |
Fail fast, fail clearly
// 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()
})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
Usercontains anAddress).Coercion & defaults — turn
"42"into42, 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.
A note on environment & config validation
// 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