Never trust input
All input coming from the client (req.body, req.query, req.params)
is untrusted data. Before using it you must:
- Validate: check that it meets the rules (fields present, correct types, valid ranges).
- Sanitize: normalize and clean (trim whitespace, lowercase an email, discard extra fields).
function validateUser(data) {
const errors = [];
if (!data.email) errors.push("Email is missing");
if (typeof data.age !== "number" || data.age < 0) {
errors.push("Age must be a non-negative number");
}
return errors;
}
Schemas (Zod / Joi, conceptual)
Validating by hand becomes repetitive. Schema libraries declare the expected shape of the data once and validate against it:
// Conceptual (Zod):
// const User = z.object({
// email: z.string().email(),
// age: z.number().int().nonnegative(),
// });
// const result = User.safeParse(data);
The schema describes what is expected, not how to check it step by step. Joi offers an equivalent idea with different syntax.
Respond 400 with clear details
When validation fails, respond with 400 (Bad Request) and include what failed, so the client can fix it:
const errors = validateUser(req.body);
if (errors.length > 0) {
return res.status(400).json({ error: "Invalid data", details: errors });
}
Returning a list of errors (instead of stopping at the first one) is more useful: the client sees all the problems at once.
Examples
validateUser returns the list of problems
function validateUser(data) {
const errors = [];
if (!data.email) errors.push("Email is missing");
if (typeof data.age !== "number" || data.age < 0) {
errors.push("Age must be a non-negative number");
}
return errors;
}
console.log(validateUser({ email: "a@b.com", age: 30 })); // []
console.log(validateUser({ age: -1 })); // 2 errors