Does mongo impose any constraints on the documents that you create? Does it care if certain keys and values exist? →
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?
Ok, so we know that we need to validate on the server side. Where in our application can we place this validation logic?
We'll be looking at numbers 1 and 2. A good candidate to start with is our Schema…
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. →
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, 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');
});
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!");
});
});
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>
Let's try inserting… and checking our database:
katy purry
and 3
bill furry
and idk!
bill furry
In Mongoose, Validation is defined in the SchemaType.
Remember that err
object in our save callback? Mongoose will populate the error object if:
Let's look at some built-in Mongoose schema validations first, since they're a bit nicer to deal with.
Mongoose has the following built-in validators:
// required
name: {type:String, required:true}
// required with a nice error message
name: {type:String, required:[true, '{PATH} is required']}
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}']}
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$/ }}
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 }});
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.',
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?
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('/');
}
});
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>
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}}
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>
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. (!?)
Validation elsewhere in your app with express-validator.
npm install --save express-validator
In your app.js:
const validator = require('express-validator');
// after app.use(bodyParser...)
app.use(validator());
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.