Mongoose provides powerful data validation features that allow validating data integrity and correctness before saving to the database. Validation can be defined at the Schema level or with custom validators.
Built-in Validators
1. Required Validation
javascriptconst userSchema = new Schema({ name: { type: String, required: [true, 'Name is required'] }, email: { type: String, required: true } });
2. Type Validation
javascriptconst userSchema = new Schema({ age: Number, isActive: Boolean, birthDate: Date });
3. Enum Validation
javascriptconst userSchema = new Schema({ status: { type: String, enum: ['active', 'inactive', 'pending'], enum: { values: ['active', 'inactive', 'pending'], message: '{VALUE} is not a valid status' } } });
4. Range Validation (min, max)
javascriptconst userSchema = new Schema({ age: { type: Number, min: [0, 'Age must be at least 0'], max: [120, 'Age cannot exceed 120'] }, score: { type: Number, min: 0, max: 100 } });
5. Length Validation (minlength, maxlength)
javascriptconst userSchema = new Schema({ username: { type: String, minlength: [3, 'Username must be at least 3 characters'], maxlength: [20, 'Username cannot exceed 20 characters'] } });
6. Regex Validation (match)
javascriptconst userSchema = new Schema({ email: { type: String, match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, 'Please fill a valid email address'] }, phone: { type: String, match: /^[0-9]{10}$/, message: 'Phone number must be 10 digits' } });
7. Unique Validation
javascriptconst userSchema = new Schema({ email: { type: String, unique: true, index: true } });
8. Default Values
javascriptconst userSchema = new Schema({ status: { type: String, default: 'active' }, createdAt: { type: Date, default: Date.now } });
Custom Validators
Single Field Validator
javascriptconst userSchema = new Schema({ password: { type: String, validate: { validator: function(v) { return v.length >= 8; }, message: 'Password must be at least 8 characters long' } } });
Async Validator
javascriptconst userSchema = new Schema({ email: { type: String, validate: { validator: async function(v) { const user = await this.constructor.findOne({ email: v }); return !user || user._id.toString() === this._id.toString(); }, message: 'Email already exists' } } });
Multi-field Validator
javascriptconst userSchema = new Schema({ password: String, confirmPassword: String }); userSchema.path('confirmPassword').validate(function(v) { return v === this.password; }, 'Passwords do not match');
Validation Timing
Validation is automatically triggered at:
save()- When saving documentvalidate()- When explicitly calling validationvalidateSync()- When synchronously validating
javascriptconst user = new User({ name: '', age: -5 }); try { await user.save(); } catch (err) { console.log(err.errors.name.message); // "Name is required" console.log(err.errors.age.message); // "Age must be at least 0" }
Skipping Validation
In some cases, validation can be skipped:
javascript// Skip validation when saving await user.save({ validateBeforeSave: false }); // Skip validation when updating await User.findByIdAndUpdate(id, { age: 25 }, { runValidators: false });
Validation Error Handling
javascriptuserSchema.pre('validate', function(next) { if (this.password !== this.confirmPassword) { this.invalidate('confirmPassword', 'Passwords do not match'); } next(); }); // Catch validation errors try { await user.save(); } catch (err) { if (err.name === 'ValidationError') { Object.keys(err.errors).forEach(field => { console.log(`${field}: ${err.errors[field].message}`); }); } }
Best Practices
- Define validation rules at Schema level
- Provide clear error messages
- Use async validators to check uniqueness
- Validate on both frontend and backend
- Consider performance impact, avoid overly complex validation
- Use custom validators for business logic
- Log validation failures