Mongoose validation: built-in and custom validators
required, min, max, minLength, maxLength, enum, match regex, custom validator function, ValidationError, async validators
Built-in validators run before every save
Mongoose runs all schema validators synchronously before persisting a document. If any validator fails, a ValidationError is thrown, the document is not written to MongoDB, and the promise rejects. This gives you a clean, centralized place to define data rules without scattering checks across your route handlers.
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required'],
match: [/^\S+@\S+\.\S+$/, 'Email format is invalid'],
lowercase: true,
unique: true
},
age: { type: Number, min: [18, 'Must be 18+'], max: 120 },
role: {
type: String,
enum: { values: ['user', 'admin', 'mod'], message: 'Invalid role' },
default: 'user'
},
username: { type: String, minLength: 3, maxLength: 30 }
})
// Custom validator โ return true for valid, false for invalid
const orderSchema = new mongoose.Schema({
items: {
type: [String],
validate: {
validator: arr => arr.length > 0,
message: 'Order must contain at least one item'
}
}
})Handling ValidationError in route handlers
try {
await new User({ email: 'notvalid', age: 10 }).save()
} catch (err) {
if (err.name === 'ValidationError') {
const msgs = Object.values(err.errors).map(e => e.message)
res.status(400).json({ errors: msgs })
}
}One important subtlety: Mongoose's unique: true on a schema field creates a unique index in MongoDB, but uniqueness is enforced by the database โ not by Mongoose's validator chain. A duplicate key error throws a different error type (MongoServerError with code 11000) rather than a ValidationError. Always handle both error types separately in your route error handling middleware.
