Implementing Validation
1. Installing express-validator
| Package | Style |
express-validator | Chainable per-field |
zod | Schema-based, TS-first RECOMMENDED |
joi | Object schema (legacy hapi) |
ajv | JSON Schema (OpenAPI alignment) |
2. Creating Validation Rules
Example: Per-field chains
import { body, query, param } from "express-validator";
const createUserRules = [
body("email").isEmail().normalizeEmail(),
body("password").isLength({ min: 12 }).matches(/[A-Z]/).matches(/\d/),
body("age").optional().isInt({ min: 13, max: 120 }),
query("ref").optional().isAlphanumeric()
];
3. Chaining Validation Methods
| Method | Purpose |
.exists() | Field present |
.notEmpty() | Not empty string |
.isEmail() | Valid email |
.isURL() | Valid URL |
.isLength({min,max}) | String length |
.isInt({min,max}) | Integer range |
.isUUID() | UUID v1-5 |
.isISO8601() | Date string |
.isIn([...]) | Whitelist |
.matches(regex) | Regex pattern |
.withMessage(msg) | Override error msg |
4. Using Custom Validators
Example: Async DB check
body("email").isEmail().custom(async (email) => {
const exists = await db.users.findByEmail(email);
if (exists) throw new Error("Email already in use");
})
| Sanitizer | Effect |
.trim() | Strip whitespace |
.escape() | HTML entity escape |
.normalizeEmail() | Lowercase, gmail dots removed |
.toInt() | Coerce to number |
.toBoolean() | "true"/"1" → true |
.toDate() | String → Date |
6. Handling Validation Results
Example: Reusable handler
import { validationResult } from "express-validator";
export function handleValidation(req, res, next) {
const errors = validationResult(req);
if (errors.isEmpty()) return next();
res.status(422).json({ errors: errors.array({ onlyFirstError: true }) });
}
app.post("/users", createUserRules, handleValidation, controller.create);
7. Returning Validation Errors
| Method | Output |
errors.array() | [{type, msg, path, value, location}] |
errors.array({onlyFirstError:true}) | One error per field |
errors.mapped() | Object keyed by field name |
errors.formatWith(fn) | Custom shape |
8. Creating Reusable Validators
Example: Shared rules
// validators/common.js
export const idParam = param("id").isUUID().withMessage("Invalid ID");
export const pagination = [
query("page").optional().isInt({ min: 1 }).toInt(),
query("limit").optional().isInt({ min: 1, max: 100 }).toInt()
];
// usage
router.get("/:id", idParam, handleValidation, controller.get);
router.get("/", pagination, handleValidation, controller.list);
9. Validating Nested Objects
Example: checkSchema
import { checkSchema } from "express-validator";
const orderSchema = checkSchema({
"customer.email": { isEmail: true, errorMessage: "Invalid email" },
"items": { isArray: { options: { min: 1 } } },
"items.*.sku": { isString: true, notEmpty: true },
"items.*.qty": { isInt: { options: { min: 1 } } }
});
10. Conditional Validation
Example: optional + if
body("phone").optional({ values: "falsy" }).isMobilePhone("any");
body("address").if(body("shipping").equals("true")).notEmpty();
| Modifier | Behavior |
.optional() | Skip if undefined |
.optional({values:"falsy"}) | Skip if falsy ("", 0, null) |
.if(cond) | Apply only when condition true |
11. Implementing Async Validation
Example: Multiple async checks
body("username")
.isAlphanumeric()
.isLength({ min: 3, max: 20 })
.custom(async (username) => {
if (RESERVED_NAMES.includes(username)) throw new Error("Reserved username");
if (await db.users.findByUsername(username)) throw new Error("Username taken");
return true;
})
Note: Async validators run in parallel within a chain — keep them idempotent and side-effect-free.