56
votes

I'm trying to decide how I want to handle validation errors in Mongoose.

Custom error messages using node-validator

I have defined my own validation rules using node-validator, for example:

UserSchema.path('username')
  .validate(function (username) {
    return validator.check(username).notEmpty()
  }, 'Username cannot be blank')

Which will generate an error that looks like this:

  username: 
   { message: 'Validator "Username cannot be blank" failed for path username',
     name: 'ValidatorError',
     path: 'username',
     type: 'Username cannot be blank' },

Using mongoose-validator

However, node-validator provides its own error messages. If I use the mongoose-validator Node module to plug node-validator directly into my schema, then I can use these error messages directly instead:

var UserSchema = new Schema({
  name: { type: String, validate: [validate('notEmpty')] }
});

Which will generate an error message that looks like:

  name: 
   { message: 'Validator "String is empty" failed for path name',
     name: 'ValidatorError',
     path: 'name',
     type: 'String is empty' } }

I can also provide a custom error message here too:

var UserSchema = new Schema({
  name: { type: String, validate: [validate({message: 'Name cannot be blank' }, 'notEmpty')] }
});

Mongoose required flag

Mongoose lets you define a field as required:

var UserSchema = new Schema({
  name: { type: String, required: true }
});

Which will generate an error message that looks like:

  name: 
   { message: 'Validator "required" failed for path name',
     name: 'ValidatorError',
     path: 'name',
     type: 'required' } }

The question

It feels as if these validators want you to use their built-in error messages. For instance, I want to declare a field as required as seen above, but I can't find a way of customising the error message. And the mongoose-validator module did not support custom messages up until very recently, which makes me think they are an anti-pattern at the model level.

What's the best way to implement these validators? Should I let them generate their own errors and then somehow interpret them afterwards?

9
... and then there is the Middleware Type Validation which will return as an error whatever type you pass to next().thanpolas
Not to mention that validation methods may be async sometimes. The only "Consistent" solution seems to revert to native driver and place a validation firewall for outside requests. Mongoose needs to provide a standard, generic interface for all errors, its own and those coming from mongo driver.S.D.

9 Answers

36
votes

At this point it seems logical to buy in to how mongoose handles errors.

You would not want your models to handle error messages. The presentation layer (controllers?) should rely on the type to decide on which is the best user-friendly message to display (i18n considered).

There's also the case where validation may happen by using a middleware. In this case, the error message that will surface up to your controller is whatever you pass to the next() callback.

So, for the case of middleware, although not documented, in order to keep a consistent validation API across your models you should directly use Mongoose's Error constructors:

var mongoose = require('mongoose');
var ValidationError = mongoose.Error.ValidationError;
var ValidatorError  = mongoose.Error.ValidatorError;

schema.pre('save', function (next) {
  if (/someregex/i.test(this.email)) {
    var error = new ValidationError(this);
    error.errors.email = new ValidatorError('email', 'Email is not valid', 'notvalid', this.email);
    return next(error);
  }

  next();
});

That way you are guaranteed a consistent validation error handling even if the validation error originates from a middleware.

To properly match error messages to types I'd create an enum which would act as a static map for all possible types:

// my controller.js

var ValidationErrors = {
  REQUIRED: 'required',
  NOTVALID: 'notvalid',
  /* ... */
};


app.post('/register', function(req, res){
  var user = new userModel.Model(req.body);

  user.save(function(err){
    if (err) {
      var errMessage = '';

      // go through all the errors...
      for (var errName in err.errors) {
        switch(err.errors[errName].type) {
          case ValidationErrors.REQUIRED:
            errMessage = i18n('Field is required');
            break;
          case ValidationErrors.NOTVALID:
            errMessage = i18n('Field is not valid');
            break;
        }
      }
      res.send(errMessage);

    }
  });
});
18
votes

I know the validator plugins are probably helpful, but I think the mongoose validation stuff is more intimidating than it really is complicated. It definitely looks complicated from the outside but once you start tearing into it, it's not so bad.

If you check out the code below, you'll see an example of how a custom error message can be returned using built-in validators.

All you have to do is set a second parameter, with your own custom error message, when setting up your fields.

Checkout the required and minlength and maxlength fields below to see how I've setup a custom error message, and then check out the methods below as to how the error object can be accessed or sent to the front end:

// Grab dependencies:
var mongoose = require('mongoose');

// Setup a schema:
var UserSchema = new mongoose.Schema (
    {
        username: {
            type: String,
            minlength: [2, 'Username must be at least 2 characters.'],
            maxlength: [20, 'Username must be less than 20 characters.'],
            required: [true, 'Your username cannot be blank.'],
            trim: true,
            unique: true,
            dropDups: true,
        }, // end username field
    },
    {
        timestamps: true,
    },
);

// Export the schema:
module.exports = mongoose.model('User', UserSchema);

The above sets up our fields to have custom error messages. But how do we access them or send them to our front end? We could have the following method setup in our server controller, whose response data is sent back to angular:

var myControllerMethods = {
    register : function(req, res) {
        // Create a user based on the schema we created:
        User.create(req.body)
            .then(function(newUser) {
                console.log('New User Created!', newUser);
                res.json(newUser);
            })
            .catch(function(err) {
                if (err.name == 'ValidationError') {
                    console.error('Error Validating!', err);
                    res.status(422).json(err);
                } else {
                    console.error(err);
                    res.status(500).json(err);
                }
            })
    },
};

If you ran the code above, and any of our mongoose validators did not pass, the error (err) object will be grabbed by the .catch() in the promise. If you console log this error, you'll see in that object is our custom message, depending upon which error got flagged.

Note: The above example is just for adding custom validation messages to the already built-in validations that Mongoose possesses (like required, minlength, maxlength and so forth).

If you want to create more advanced validations, such as validating fields against regex patterns or the like, then you'll have to create custom validator functions.

See the "Custom Validators" section at this link for a great example of how to add a validator right onto your field: http://mongoosejs.com/docs/validation.html.

Note: You can also use "pre save hooks" and "instance methods", but this is beyond the scope of this question and the built in validators and "Custom Validators" (link aforementioned) are easier routes.

Hope this helps!

4
votes

From Mongoose: https://github.com/leepowellcouk/mongoose-validator

Error Messages Custom error messages are now back in 0.2.1 and can be set through the options object:

validate({message: "String should be between 3 and 50 characters"}, 'len', 3, 50)


How I implemented this:

var emailValidator = [validate({message: "Email Address should be between 5 and 64 characters"},'len', 5, 64), validate({message: "Email Address is not correct"},'isEmail')];

var XXXX = new Schema({
email : {type: String, required: true, validate: emailValidator} }); 

My front end deals with required, so I don't ever expect the mongoose "required" error to make it to the user, more of a back end safe guard.

3
votes

The question you need to ask yourself is who is responsible for causing the error in the first place?

If this happens in your system, that you are in control over, just let the errors hit you like you normally would, and weed the bugs out as you go along, but I suspect you are doing an application which is facing real world users and you want to sanitize their inputs.

I would recommend that client side you check that the input is correct before you send it to your server, and you show nice helper messages like "Your username must be between x and y characters".

Then on the server side you expect that in 99% of the cases the input will come directly from your sanitizing client, and so you still validate it using the techniques you already suggested, but if there is an error you simply return a general error message to the user interface - since you trust that your user interface would have shown helper messages so the validation error must be caused by a bug or a hacking attempt.

Remember to log all server side validation errors as they could be severe bugs or someone looking for exploits.

2
votes

Warning: As of Mongoose 4.1.3 the signature for function ValidatorError has completely changed and the information below is no more applicable:

As of Mongoose 3.8.12 the signature for function ValidatorError is:

function ValidatorError (path, msg, type, val) 

Where type can be either "notvalid" or "required"

For example if your "email" field validation raises a validation error, you can simply do:

var error = new ValidationError(this);
error.errors.email = 
      new ValidatorError('email', "Your err message.", 'notvalid', this.email);
2
votes

As of mongoose 4.5.0 Document#invalidate returns a ValidationError. See this https://github.com/Automattic/mongoose/issues/3964

Also, when trying to invalidate on a findOneAndUpdate query hook, you can do:

// pass null because there is no document instance
let err = new ValidationError(null)
err.errors[path] = new ValidatorError({
    path: 'postalCode',
    message: 'postalCode not supported on zones',
    type: 'notvalid',
    value,
})
throw(err)
2
votes

See the hmv package, which helps you customize the mongoose error message templates, including unique index errors :

template : {PATH_NAME} must be at least {MIN_LENGTH} characters long
schema   : { fullname : { type : String, min : 3, $name : 'Full name' } }
message  : Full name must be at least 3 characters long

template : {PATH_NAME} {VALUE} have been used, please choose another
schema   : { username : { type : String, unique : true } }
message  : username MrBean have been used, please choose another

And specifically supports localization, ex in Vietnamese :

template : {PATH_NAME} dài ít nhất {MIN_LENGTH} kí tự
schema   : { fullname : { type : String, min : 3, $name : 'tên tài khoản' } }
message  : Tên tài khoản dài ít nhất 5 kí tự

Good point is that you only need to customize the message template once, instead of customizing every field of every schema in the previous approach.

1
votes

you can also use joi here. It is actually very useful package for validating the schema in node app.

for eg:

const Joi = require("joi");

const validateUser = user => {

 const Schema = {
  email: Joi.string().email().required(),
  name: Joi.string().min(3).max(20).required(),
  password: Joi.string().min(8).max(25).required()
 }

 return Joi.validate(user, Schema);

}
0
votes

// my controller.js or whatever your route handler file is
// This Handles multiple Schema Level Validations in 1 line

// user object created from model
let user = new Users({
firstName: req.body.firstName,
lastName: req.body.lastName 
...

});

  // Since the Error response are in <Model Name>:<Schema Name>:<Error Message>, ... 
  var err = user.validateSync();
  if (err && err.message) return res.send(err.message.split(':')[2].split(',')[0]);