Validation with Joi
Joi is a mature, expressive schema-description and validation library. You build a schema with a fluent, chainable API, then validate data against it. Joi predates the TypeScript-first wave and remains popular for its breadth of built-in rules and its framework-agnostic design — it validates any JavaScript value, not just Express requests. This page covers building schemas, the validation result, options, and integrating Joi as Express middleware.
Setup and a first schema
npm install joi
import Joi from 'joi'
const userSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150),
role: Joi.string().valid('user', 'admin').default('user'),
tags: Joi.array().items(Joi.string()).max(10),
})Validating — the result object
const { error, value } = userSchema.validate(req.body)
if (error) {
// error.details is an array of { message, path, type }:
const details = error.details.map((d) => ({
field: d.path.join('.'),
message: d.message,
}))
return res.status(422).json({ error: 'Validation failed', details })
}
// 'value' is the validated + coerced + defaulted data — use THIS:
createUser(value)Key validation options
const { error, value } = userSchema.validate(req.body, {
abortEarly: false, // report ALL errors, not just the first
stripUnknown: true, // remove keys not in the schema (anti mass-assignment)
convert: true, // coerce types: "42" → 42, "true" → true (default on)
presence: 'optional', // default presence for all keys
})Option | Effect |
|---|---|
| Collect every error, not just the first |
| Drop unknown keys — defends against mass assignment |
| Permit (keep) unknown keys instead of erroring |
| Type coercion ("5" → 5); on by default |
Reusable validation middleware
// Factory: validate a request part against a Joi schema.
const validate = (schema, part = 'body') => (req, res, next) => {
const { error, value } = schema.validate(req[part], {
abortEarly: false,
stripUnknown: true,
})
if (error) {
return res.status(422).json({
error: 'Validation failed',
details: error.details.map((d) => ({
field: d.path.join('.'),
message: d.message,
})),
})
}
req[part] = value // replace with cleaned, coerced data
next()
}
app.post('/users', validate(userSchema), createUserHandler)
app.get('/users', validate(listQuerySchema, 'query'), listUsersHandler)Powerful conditional & cross-field rules
const schema = Joi.object({
password: Joi.string().min(8).required(),
// Must match 'password':
confirm: Joi.string().valid(Joi.ref('password')).required()
.messages({ 'any.only': 'Passwords must match' }),
// Required only when 'type' is 'business':
type: Joi.string().valid('personal', 'business'),
taxId: Joi.when('type', {
is: 'business',
then: Joi.string().required(),
otherwise: Joi.forbidden(),
}),
}).with('email', 'name') // if email present, name must be tooJoi vs the alternatives
Joi | Zod | express-validator | |
|---|---|---|---|
Style | Fluent schema | Fluent schema | Per-field middleware chain |
TypeScript types | Add-on/manual | Inferred from schema | Manual |
Scope | Any JS value | Any JS value | Express requests only |
Strength | Breadth of rules, conditionals | Type inference | Tight Express integration |