Episode 3 — NodeJS MongoDB Backend Architecture / 3.10 — Input Validation
3.10 — Quick Revision: Input Validation
A concise cheat sheet covering validation concepts, express-validator, Zod, and error handling patterns.
< Interview Questions | README
Core Concepts
| Concept | Definition |
|---|---|
| Validation | Checking data meets rules — accept or reject |
| Sanitization | Transforming data to be safe/clean |
| Defense in depth | Validate at every boundary (client, middleware, schema, DB) |
| Mass assignment | Vulnerability from passing raw req.body to database |
| Trust boundary | Any point where data enters from an external source |
express-validator Cheat Sheet
npm install express-validator
const { body, param, query, validationResult } = require('express-validator');
// Field targets
body('field') // req.body
param('field') // req.params
query('field') // req.query
// Common validators
.notEmpty() // Not empty
.isEmail() // Valid email
.isLength({ min: 2, max: 50}) // String length
.isInt({ min: 0, max: 100 }) // Integer range
.isFloat({ min: 0 }) // Float
.isURL() // Valid URL
.isMongoId() // MongoDB ObjectId
.isStrongPassword() // Complex password
.isIn(['a', 'b', 'c']) // Whitelist
.matches(/regex/) // Regex match
.isBoolean() // Boolean
// Sanitizers
.trim() // Remove whitespace
.escape() // HTML escape
.normalizeEmail() // Lowercase + normalize
.toInt() // Convert to integer
.toFloat() // Convert to float
.toLowerCase() // Lowercase
// Custom validator
.custom(async (value, { req }) => {
const exists = await Model.findOne({ field: value });
if (exists) throw new Error('Already exists');
return true;
})
// Error messages
.withMessage('Custom error message')
// Extract errors
const errors = validationResult(req);
errors.isEmpty() // boolean
errors.array() // [{ msg, path, value, location }]
errors.mapped() // { field: { msg, ... } }
Zod Cheat Sheet
npm install zod
const { z } = require('zod');
// Primitives
z.string() z.number() z.boolean() z.date()
// String refinements
.email() .url() .uuid() .min(n) .max(n) .length(n)
.regex(/pattern/) .trim() .toLowerCase() .startsWith()
// Number refinements
.int() .positive() .nonnegative() .min(n) .max(n) .multipleOf(n)
// Object
z.object({ field: z.string() })
// Array
z.array(z.string()).min(1).max(10)
// Enum
z.enum(["a", "b", "c"])
// Optional / Nullable / Default
.optional() // T | undefined
.nullable() // T | null
.default(val) // Uses val if undefined
// Parsing
schema.parse(data) // Returns data or throws ZodError
schema.safeParse(data) // Returns { success, data/error }
// Type inference
type MyType = z.infer<typeof mySchema>;
// Transform
.transform(val => val * 100)
// Preprocess (before validation)
z.preprocess(val => Number(val), z.number())
// Custom validation
.refine(val => val > 0, { message: "Must be positive" })
.refine(async val => !(await exists(val)), { message: "Taken" })
// Use parseAsync() / safeParseAsync() for async refine
Error Response Format
// Standard success
{ "success": true, "data": { ... } }
// Standard error
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"details": {
"email": ["Must be a valid email"],
"password": ["Too short", "Needs uppercase"]
}
}
}
| HTTP Status | Use For |
|---|---|
| 400 | Malformed request / validation errors |
| 401 | Authentication required |
| 403 | Forbidden (authorized but not allowed) |
| 404 | Resource not found |
| 409 | Conflict (duplicate) |
| 422 | Semantically invalid (alternative to 400) |
| 429 | Rate limit exceeded |
| 500 | Server error |
Reusable Middleware Pattern
// express-validator
const validate = (validations) => async (req, res, next) => {
await Promise.all(validations.map(v => v.run(req)));
const errors = validationResult(req);
if (errors.isEmpty()) return next();
res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', details: errors.array() } });
};
// Zod
const zodValidate = (schema) => (req, res, next) => {
const result = schema.safeParse(req.body);
if (result.success) { req.validatedBody = result.data; return next(); }
res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', details: result.error.flatten().fieldErrors } });
};
express-validator vs Zod
| Feature | express-validator | Zod |
|---|---|---|
| TypeScript types | Manual | z.infer |
| Framework | Express only | Any |
| Async validation | .custom(async) | .refine(async) + parseAsync |
| Sanitization | Built-in | .transform() |
| Size | ~50KB | ~13KB |
| Best for | Express JS projects | TypeScript, shared schemas |
Security Reminders
- Always validate server-side — client validation is UX only
- Whitelist fields — never pass raw
req.bodyto database - Type-check inputs — strings, numbers, booleans (prevents NoSQL injection)
- Limit payload size —
express.json({ limit: '1mb' }) - Escape HTML — prevents stored XSS
- Generic auth errors — "Invalid credentials" not "Wrong password"
- Never log passwords/tokens in error details