Validation, Error Handling, Forms

CSCI-UA.0480-008

Topics

  • where to implement validation
  • validation in our schema
  • sending errors back
  • express-validate

MongoDB and Creating Documents

Does mongo impose any constraints on the documents that you create? Does it care if certain keys and values exist?

  • mongo doesn't care at all!
  • you can insert whatever document you want, with whatever key/value pairs
  • it doesn't even matter if there's no database yet or no collection yet, it'll create those for you (remember? →)


MongoDB is Pretty Laid Back

Maybe too laid back. Sooo… do we just let users enter in whatever data they want?

We probably shouldn't do that, of course. So if our database doesn't deal with constraints and validations, who's going to be responsible for doing that?

The application layer! But where in our application layer - client-side (in our form, through constrained form fields) or server-side (in our express app)? Why?

  • at the very least, server side (you can always bypass the frontend by sending a request directly with something like the request module for node, curl, etc.)
  • ideally, however, you'd want validation on both the client and the server side (the sooner the user can get feedback, the better)
  • which poses an interesting problem…
  • syncing validation

Server Side Validation

Ok, so we know that we need to validate on the server side. Where in our application can we place this validation logic?

  1. in our database abstraction layer (our Mongoose schema)
  2. in our controller (our Express router)
  3. some other intermediary object / layer (maybe we'll create a form object responsible for mediating between Mongoose and our frontend)


We'll be looking at numbers 1 and 2. A good candidate to start with is our Schema…

Rejected by Mongoose!

Mongoose has facilities for validation, and we're already sort of using them. Let's see this in action by setting up a quick schema and form.

  • let's go back to our cat schema…
    • cat name
    • cat age
  • make sure one of the fields is a Number


In db.js….


const mongoose = require('mongoose');

// back to cats!
const CatSchema = new mongoose.Schema({
  name: {type:String},
  age: Number
});

const Cat = mongoose.model('Cat', CatSchema);
mongoose.connect('mongodb://localhost/catdb'); 

And the Remainder of the Setup on the Server

And, of course, require in app.js:


require('./db');

Set up your route and handlers in index.js:


const mongoose = require('mongoose');
const Cat = mongoose.model('Cat');

router.get('/', function(req, res) {
  res.render('index');
});

And the Post


router.post('/', function(req, res) {
  console.log(req.body);
  const cat = new Cat({
    name: req.body.name, 
    age: req.body.age,
  });
  cat.save(function(err, cat, count) {
    console.log("Saved!");
  });
});

Lastly, Our Form

In views/index.hbs


<form method="POST" action="">
<div><label>Name</label> <input type="text" name="name"></div>
<div><label>Age</label> <input type="text" name="age"></div>
<input type="submit"></div>
</form>

Whew!

Let's try inserting… and checking our database:

  • katy purry and 3
  • bill furry and idk!
    What happened to the second one? How can we find out?
  • insert didn't work for bill furry
  • log the error in our save callback
  • looks like there was a cast error, and an error object

Mongoose Validators, Types

In Mongoose, Validation is defined in the SchemaType.

  • it occurs when a document attempts to be saved, after defaults have been applied
  • embedded document validation occurs as well


Remember that err object in our save callback? Mongoose will populate the error object if:

  • the document doesn't pass built-in Mongoose schema validations
  • the document doesn't pass custom Mongoose validations
  • the document doesn't adhere to the types declared in the schema


Let's look at some built-in Mongoose schema validations first, since they're a bit nicer to deal with.

Built-In Validators

Mongoose has the following built-in validators:

  • all schema types can be required
  • Numbers can have a min and max
  • Strings can be constrained to a specific set of strings (an enum) or to a specific match
  • these all involve setting the property/field in your schema to an object

Required


// required
name: {type:String, required:true}

// required with a nice error message
name: {type:String, required:[true, '{PATH} is required']}
  • use the required property with a boolean
  • optionally set the value to an array, with the first element a boolean, and the second a custom error message
    • {PATH} can optionally be used a placeholder for the field/property name

Min and Max

For numbers, we have min and max…


// min
age: {type:Number, min:[0, '{PATH} must be greater than {MIN}']}

// max
age: {type:Number, max:[0, '{PATH} must be less than {MAX}']}
  • can gave just number or number and custom message
  • additional placeholders include {MIN} and {MAX}
  • you can have a min and max on the same field

Enum

For strings, we have enum and match:


// enum
temperament: { type: String, required: true, enum: ['annoying', 'playful'] }

//enum with message
const enumOptions = {values:['annoying', 'playful'], message:'{VALUE} is not a valid temperament'} ;

temperament: { type: String, required: true, enum: enumOptions}

// match
nickname: { type: String, match: /^\w\w\w$/ }}
  • enum has an array of possible values
  • note that to include a custom message, you must use an object with a values and message property
  • match has regex for validation

Summary of Placeholders

  • {PATH} - the property name
  • {VALUE} - the property's value
  • {TYPE} - the validator type ("regexp", "min", or "user defined")
  • {MIN} - the specified minimum
  • {MAX} - the specified maximum

Need More?

Custom validation also exists. From the docs:


// make sure every value is equal to "something"
function validator (val) {
	return val == 'something';
}
new Schema({ name: { type: String, validate: validator }});

// with a custom error message
const custom = [validator, 'Uh oh, {PATH} does not equal "something".']
new Schema({ name: { type: String, validate: custom }});
  • create a function that returns true or false
  • set that has the value of the validate property in your schema

Errors

When we log out the error object for validation errors, we get:


{ [ValidationError: Validation failed]
	message: 'Validation failed',
	name: 'ValidationError',
	errors:
		{ temperament:
			{ [ValidatorError: Path `temperament` is required.]
				message: 'Path `temperament` is required.',
				name: 'ValidatorError',
				path: 'temperament',
				type: 'required',
				value: '' },
		name:
			{ [ValidatorError: Path `name` is required.]
				message: 'Path `name` is required.',

Handling Errors

Now that we have errors what should we do with them? Keep them to ourselves?

We should probably show the user if there's an issue with their input

How do you think we can show errors on the frontend?

  • check if there's an error in the router
  • use the error object to send error messages to our view through the context
  • display the errors
    • look at everything in errors
    • use specific errors.propertyname

In Our Router…

Do we have an error? Check the err object in our callback. If we do, render form again with errors passed in.


  const cat = new Cat({
    name: req.body.name, 
    temperament: req.body.temperament, 
    age: req.body.age
  });
  cat.save(function(err, cat, count) {
    console.log(err, cat, count);
    if (err) { 
      res.render('index', { cat:cat, err: err });
    } else {
      res.redirect('/'); 
    }
  });

In Our View

We can loop through all errors and display them above the form…


{{#if err}}
<ul>
{{#each err.errors}}
<li>{{message}}</li>
{{/each}}
</ul>
{{/if}}
</form>

Another Way

Or we can go field by field. Above each form element, check if there's an error for that element.


{{#if err.errors.name}} 
<div class="error">
{{err.errors.name.message}}
</div>
{{/if}}

Form Fields

If you're sending errors back, should the form elements be prefilled?

It'd be courteous to fill them in with what the user had originally submitted:

You can access the value of the field in the error object…


<div>
<label>Name</label> 
<input type="text" name="name" value="{{err.errors.name.value}}">
</div>

Great!

But let's try a type error. Let's put in a string for a number. What do we get back?

That's not the same error object!


{ [CastError: Cast to number failed for value "one" at path "age"]
message: 'Cast to number failed for value "one" at path "age"',
name: 'CastError',
type: 'number',
value: 'one',
path: 'age' 
}

Unfortunately, we'd have to handle that through custom validation. (!?)

Another Option - Express Validator

Validation elsewhere in your app with express-validator.

  • it's just middleware
  • you can replicate and augment your Schema validation using built-in validators
    • isInt
    • notEmpty
    • etc.

npm install --save express-validator

In your app.js:


const validator = require('express-validator');

// after app.use(bodyParser...)
app.use(validator());

Express Validator Continued

One place you can put it is in your route handler for create. For example… add validators and collect the errors in an error object.


req.checkBody('age').notEmpty().isInt();
errors = req.validationErrors(true);

Aaaand… use the error object to send back go your form.