Mongoose Validation and Custom Validators Explained
Mongoose has built-in validation and lets you write custom validators. Here is how both work.
Mongoose Validation and Custom Validators Explained
Validation at the schema level prevents bad data at write time. Mongoose has built-in validators and lets you write your own.
Built-in Validators
- required: field must be present.
- min and max: for numbers.
- minlength and maxlength: for strings.
- enum: value must be in the list.
- match: value must match a regex.
const userSchema = new mongoose.Schema({
email: { type: String, required: true, match: /^\S+@\S+\.\S+$/ },
age: { type: Number, min: 18, max: 120 },
password: { type: String, required: true, minlength: 8 },
gender: { type: String, enum: ['male', 'female', 'other'] }
});
When Validation Runs
Validation runs on .save() and Model.create(). It does NOT run on findByIdAndUpdate, updateOne, or updateMany by default. To run validation on updates, pass { runValidators: true }:
await User.findByIdAndUpdate(id, update, { runValidators: true, new: true });
Custom Validators
Use the validate property with a validator function and a message:
const userSchema = new mongoose.Schema({
phone: {
type: String,
validate: {
validator: function(v) {
return /^\+?\d{10,15}$/.test(v);
},
message: props => `${props.value} is not a valid phone number`
}
}
});
Async Validators
Validators can return a promise:
email: {
type: String,
validate: {
validator: async function(v) {
const count = await mongoose.models.User.countDocuments({ email: v });
return count === 0;
},
message: 'Email already in use'
}
}
Be careful with this; it adds a query on every save.
Custom Validation With a Library
Many teams use Zod or express-validator at the middleware layer instead. Schema validation is a backup; middleware validation is the primary check. Use both for defense in depth.
Handling Validation Errors
When validation fails, .save() throws a ValidationError. The error has an errors object keyed by field:
try {
await user.save();
} catch (err) {
if (err.name === 'ValidationError') {
const fields = Object.keys(err.errors);
// return 400 with the field list
}
}
The Takeaway
Mongoose has built-in validators (required, min, max, minlength, maxlength, enum, match) and lets you write custom ones with the validate property. Validation runs on .save() and .create() by default; pass runValidators: true for updates. Use Zod at the middleware layer as the primary check and Mongoose validation as a backup.
required, min, max (for numbers), minlength, maxlength (for strings), enum (value must be in the list), and match (value must match a regex). Use them at the schema level to prevent bad data at write time.
On .save() and Model.create() by default. It does not run on findByIdAndUpdate, updateOne, or updateMany unless you pass { runValidators: true }. Always pass this option when updating through these methods.
Use the validate property with a validator function and a message. The function returns true or false (or a promise). The message can be a string or a function that receives props. Example: validate: { validator: v => /^\+?\d{10,15}$/.test(v), message: 'Invalid phone' }.
Use both. Zod at the middleware layer is the primary check and returns clean 400 errors. Mongoose validation is a backup that catches anything middleware missed. Defense in depth.
Catch the ValidationError thrown by .save(). It has an errors object keyed by field. Return 400 with the field list. Example: if (err.name === 'ValidationError') return res.status(400).json({ errors: Object.keys(err.errors) });
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

