Using Validation Middleware - AdarshMaurya/mongodb-and-mogoose-for-node-getting-started GitHub Wiki
Introduction
Welcome to this module on Mongoose validation. This module will introduce the built-in validators we have available to us in Mongoose. We will then introduce the concept of middleware and the development of custom validators. Next, we will discuss how to handle errors produced by the validation process and how to provide friendly error messages back to the application user. Finally, we will polish off the Express API server side of the demo application by adding in custom validation and error handling.
Built-in Validators
Let's start by looking at validation that's built into Mongoose. Because validation is defined in the Mongoose schema type itself, let's do a quick review of some of the more commonly used schema types. Those are listed here. All schema types have the built-in required validator, and we'll look at an example of that here next. Besides required, strings have the following built-in validators available as well: enum, match, minlength, and maxlength. Numbers have the additional built-in validator min and max. Now let's look at a few examples of using these Mongoose built-in validators for strings and numbers. We'll first look at some examples for the string schema type. In this example, we start with the simple customer schema we used before. This time for the name field, we've added in required: true. Notice again that we have passed the type in as an object to include the required validator with it. Not only can we specify that we want a particular field to be required right in the schema definition, but you can come back to the schema object later and add required onto a path with the schema. The path is simply the field name here. This example also shows adding in the optional customized error message. We'll talk about that more in the clips to come. Notice the signature method here where the first argument is a Boolean and then the optional message. Next, let's look at the match validator for a string. We first set up our regular expression stating that we only want to allow alpha characters, either upper or lowercase, but no numbers or other characters. The previous required validator is still used here, but we've added on the match validator as well, passing in the regular expression match object as its value. Next up, we have the enum validator for a string. As the name would imply, we are going to ask this validator to ensure that our string value is found within an enumerated list of strings, like the one shown here. If a string is entered that is not within the list, the enum validator will return false and pass a default error message for this validator to the callback. The number schema type has two other validators besides required. Those again are min and max. The first example here is used to illustrate the min validator where we've asked that our discount value of type number have a minimum value of five. Next, let's look at the max built-in number validator. With this example, we've asked that our discount have a maximum value of 60, and as you might imagine, we can use both numbers at the same time and therefore ask for a range of numbers between min and max values given.
Introduction to Middleware
Because the built-in validators we just looked at are an internal piece of Mongoose middleware, it would be good to take just a moment to introduce the concept of middleware. As you have already learned, a document is an instance of a Mongoose model. These instance documents have static helper methods available to them, some of which we have already explored in this course, such as the save method. Middleware are functions which are given control over the execution flow for the following methods: validate, save, remove, and init. We will only be discussing middleware for the save method in this module. You can think of middleware as sort of a detour on the execution flow highway on our way to saving our document. Earlier, we looked at built-in validators, and the one that is available to all schema types is required. Let's look at a validation middleware example of calling the save method on a document instance of a model based off of a schema where we have set required to true. As you now know, everything in Mongoose starts with a schema. So let's define a personSchema with three fields, first and lastName and status. The status field is just so we know if they're alive or dead. Next, we will build a model from the schema and then a document instance from the model, supplying values to the firstName and lastName fields only, skipping status. Now we will attempt to save the document. For our purposes of discussing the flow of execution, here's what would take place. Save is called; default value, if any, as defined in the schema are applied. In our example, we did not supply a value to status. It is marked as required; and therefore, the default value of Alive will be applied to that field. Now validation is performed. In this example, we are only exploring the built-in required validator, so the validation process looks to make sure that we have supplied a value to the firstName and lastName fields. If we have not, an err is generated and received by the callback. For the purposes of discussing the flow of execution around validation, here is what takes place. The model's save method is called; default values, if any, as defined in the schema are applied. In the previous example, we did not supply a value to status. It is marked as required and therefore the supplied default value will be applied to that field. Now a validation is performed. Again, in the previous example, we are only using the built-in required validator, so the validation process simply ensures that we supplied values to the firstName and lastName fields. If not, then an error is generated and received by the callback; otherwise, we're good to go, and the document will be successfully saved to the MongoDB database.
Custom Validators
Now let's look at how to build custom validators. While being able to ensure that a string, number, date, or other schema type has provided required data or that a number is within a min or max range is very useful. There are times when the built-in validators we looked at in the previous clip are just not enough. Thankfully, it's pretty easy to develop custom validators for Mongoose, and that's what this section is all about. For this example, we're going to reuse the previous personSchema, but this time, we've added a custom validator. Our validator is defined here, and we've chosen to also supply the optional errorMsg argument. As show here in this method signature, a validator will always receive the value you wish to validate as the first argument Custom validators must return true or false. True meaning the validation process passed, and naturally, false if it did not. Our sizeValidator is an array of two objects. The first is the validator function, and the second is an optional custom error message. If not supplied, a generic error message template will be used. We'll talk more about error message templates in the next clip.
Handling Validation Errors
We just saw an example of supplying a custom error message into a validator. Now let's take a look at how to handle those errors and notify the user that something went wrong, hopefully in an understandable and nice way. Let's take one more quick look at the custom sizeValidator we used in the last clip. Only this time, we'll intentionally violate the custom size validation rule and provide an obnoxiously long firstName for this Person document. When we save the new Person document instance of the Person model, validation will fail and an err object will be returned in the callback function. So what exactly does this err object contain? Let's take a look at an example of that now. This error JSON object is an example of what would be returned. Note that the custom error message here states String must be between 1 and 50, but wouldn't it be nice to tell the user which specific field this validation failed on? If the optional custom error message is not supplied, a generic error message template will be used. Not only are you allowed to completely customize the error message as we saw in the previous err object example, but you may also choose to customize the error message template for built-in validators, such as required or min and max for numbers. In this example, the built-in validation message for the Number.min validator, which is shown here, is being overwritten with a custom message. PATH is the field the invalid value was supplied to, and VALUE is the number entered by the user, and MIN tells the user what the minimum value actually is as defined in the schema. Taking one more quick look at this custom validator example from before, it's the same, except now we're telling the user which PATH or field name the invalid data was supplied to.
Adding Validation
Let's finish off the Express API server side of the demo application by adding in some validation. The standup schema currently has no validation at all. We're simply supplying the type of string on most fields and as type Date on the createdOn field with a default value set. Let's start by adding in the basic built-in required validator to our standup schema, which will leave our standup schema looking similar to this. We will test this out in Postman and show why setting required to true alone is not going to be good enough for our application. Take, for example, the workYesterday field. We've said that it's required so the user will have to enter something there, but a bunch of spaces is something, right? Just not what the business may want to see there. so we need to come up with a custom validator to test for that situation. That custom validator will be named requiredStringValidator, which is an array of two elements, the first being the test function, which returns true or false based upon the value passed in and test conditions set up. In this case, we're trimming the string to ensure that the user didn't enter a bunch of spaces then testing the length to see if it's greater than 0. If so, true is returned. If not, the second element, the Custom error message, is returned. And here is the standup schema once again, this time with the requiredStringValidator added into the workYesterday field as just one example. We will actually want to add this to the project, workToday, and impediment fields as well. The next custom validator we'll add in and test is for the teamMemberSchema. This custom validator is the size validator we've already looked at examples of in this module, but now we'll add that one in here to the teamMemberSchema to ensure that someone doesn't try to enter in a bunch of spaces again or a name that is just crazy long. Alright, it's time to open up Visual Studio Code and add in the validation we just about to talked to the demo application. We will start with the built-in required validation, which along with the custom validators, is already in place. Using Postman, we'll test first with just the built-in required validator in place then add in the custom validator to the standup and teamMember schema files. Let's get started on that.
Demo: Adding Validation
As noted earlier, the custom validators are already in place here in the standup.js schema file, as well as the teamMember.js schema file. But as you can see in each schema, we're not using them just yet. We will start our testing out with just the built-in required validator set to true and see what that does for us and why that probably isn't going to be enough for our business needs. Let's open up a terminal window and make sure that MongoDB is running. Next, change to the server folder and start the Express API server, yarn start, to kick that off. It will take a few seconds for that to spin up, but once it's done and the server is listening on localhost port 8081, we can open up Postman and test this out. I'll use the test POST previously set up to save a new standup note to the database api/standup. Only this time, in the body being sent in the request, let's change workYesterday to a bunch of spaces and send that POST request. Now we should open up Studio 3T, connect, and take a look at this new entry. Connecting to local, and then select virtualstandups, and then the standups collection, the last entry here is the one that we just posted, and you can see it was saved with all of those blank spaces. I'm going to remove this document now and return to Visual Studio Code where we'll add in the custom validator to address this problem. Back in the standupSchema, I'll add custom validation now, validate, and the name of the custom validator is up here, requiredStringValidator. We'll want this validation on other fields as well, so I'll just copy and paste that now. That's better. The custom requiredString validation is now on each of the fields that we do not want to allow users to simply enter empty spaces on. Now let's go fix the teamMemberSchema as well. We will do the same thing here in this schema, now using the sizeValidator custom validator. Validate: sizeValidator and then Ctrl+S to save this change. Let's take a look at the terminal window, and we can see that nodemon has restarted due to those schema changes. So we're good to go to retest this again in Postman now. This is the same test POST, and the spaces are still here. So let's Send this again now. And scrolling down, we can see the error message our Mongoose validation returned, so that was a success. While we're here in Postman, let's go ahead and test the change to the teamMemberSchema with the custom sizeValidator. To set this test up, I'll use the test GET to grab the list of team members, and copy one of these objects, and change this to a POST now, and clean this object up, and change the name to a random long string value, which is clearly more than 50 characters long. Oh, and we do not need this _id, and this needs to be JSON, not Text. Sending this post, and you can see that a 400 Bad Request error object was returned clearly telling us what the error was. The custom Mongoose validation we've added to the Express API server is working as expected.