Transcripts
1. Class Introduction: Welcome back to the
Express JS course, Module seven, Mangus validation. This class is a continuation of the Express JS course series. My name is Shawn Raganhi and I'm excited to guide
you through this module where we take your back
and development skills to the next level by mastering
Mangus validation. In this module, we will
focus on validation, a crucial aspect of any
robot backend system. You will learn how to implement validation in mangos to
ensure data integrity. Then you will learn utilizing
built in validators for quick and effective
constraint enforcement, then creating custom validators tailored to specific
application needs. After that, you will
learn to handle and respond to validation
errors gracefully, and finally, customize
schema options for greater flexibility
and control. This module is a game
changer for developers aiming to create reliable
and scalable applications. By mastering
Mongoose validation, you will write cleaner
error free code, reduce bugs, and ensure that your data is always
consistent and trustworthy. These are some of
the essential skills for building
production ready APIs. In this module as a project, we will continue building
the Fair Wheels application. First, you will enhance the company's API by replacing the in memory
array with Mongo DB. Then you will create
a new customer's APA, complete with full crowd
operations and validation. This hands on project will
solidify your understanding of integrating
Mongoose validation into real world applications. This module is packed
with valuable techniques and practical experience to help you build better backends. So let's dive in and
make your skills shine. See you in the first lecture.
2. Implementing Validation in Mongoose: I So this is the core schema that we defined
earlier in the section. Now by default, all
these properties that we define here,
they are optional. So if I create a course and leave out all
these properties, then save the purse
to the database, that will be perfectly
valid operation. Mongo DV doesn't
care that we have a course that doesn't have a name or doesn't have a price. Wen this lecture,
I'm going to show you how to implement validation. Now for this demo,
I'm only going to talk about the
required validator. But we have more built in validators that you are going to learn about
in the next lecture. So let's make this
name required. First, we replace the
string with an object here. We set the type to string, and then we set required, true. With this, if I create a course without a name, so
let's come in this out. At the time I try to save
this course to the database, I'm going to get an
exception. Let me show you. So first, let's go
back to the end of this file and delete,
remove course. Now come in do, create course. Now back in terminal,
let's run the application. All right. Look at what we got. We got this variation error. If you see that error, that basically
means that you have not handled that rejection. So just to refresh your memory, remember, promises can
be in three states. Initially, they are pending, then they can be either
fulfilled or rejected. In this case, we have
a rejected promise. So we have not handled
that properly. So back in the code here, when saving this course
to the database, look, the save method
returns a promise. We are awaiting there
to get the result. So with this implementation, we are only assuming
the success scenario. If the promise is rejected, we don't have any
code to handle that. So earlier, I told
you that you should put this code in
a tr cache block. So let's move this here. Add the cache block here, we get an exception, and then we can display exception
message on the canso. Now, back in the terminal, let's run the application
one more time. So we don't get a
warning anymore. Instead, we get
this error message. Forse validation fail,
path name is required. So if we have an
invalid course object, Mongoose doesn't allow us to save the course
through the database. So validation
automatically kicks in at the time we try to save a
course through the database. We can also manually
trigger the validation. Let me comment out
these two lines. This course object has
a validate method. Now this validate method, look, it returns a promise of void, so we can await it. And if our course is invalid, then we will get an exception, and we will end up
in this cache block. So let's go back to the
terminal node index has, look, we got the same
validation error. Path name is required. Now, one thing I
personally don't like about the
design of mongoose is that this varidate method
retains a promise of void. So here we don't
have any results. Idally, this validate method
should return a boolean. So we could call this is valid. And then if the
course is not valid, we could have some logic here. So this is a design
flaw and mongoose. It returns a promise of void. Now, the only option
to get that kind of boolean is to pass
a callback here. So instead of
awaiting the promise, we have to go back to the
callback pase approach. So here we pass a function
that takes an error object, and then we can check
if we have any errors. Then we can execute some logic. Now you might ask, we already
have this cache block here. So if we have any
validation errors, we can execute that
kind of logic here. That is true, but writing code like that is a
little bit messy. So I hope sometime
in the future, Mongo's team change this
method to Dana Boolean. Now that aside, let's move this code and go back
to our original code. One thing I need
to clarify here is that this validation that we implemented on the name property is only meaningful in mongoos. Mongo DB doesn't care
about this name property. So if we have worked
with databases like SQL server or MySQL, you know that in
these databases, we can define validation
at a database level. For example, in
our courses table, we're going to
have a name column and we can mark that
column as required. With that, we cannot store a course without the
name in our database. In Mongo DB, we don't have that. Mongo Dew doesn't care
about any of this stuff. So this validation
we have implemented here is only
meaningful in mongoos. At the time we try
to save a course, mongoose runs the
validation logic. And if the course is not valid, it won't save it
to the database. Now, one last thing I need to clarify here before we
finish this lecture. Earlier in the section
about Express, I introduced you to a
nought package called Joy. So you might be asking when we have two kinds of validation. Should we use Joy or should we use mongoose as validation? The answer is both. These kind of validations
complement each other. So we use joy in
our restful APIs. We use that as the first
attack to make sure the data that the client is
sending us is valid data. But we still need this
kind of validation in mangos to make sure
that the data we save to the database is in
the right shape because it is possible that the client sends us a valid course in the
body of the request. But when we create a course
object in our SDDP service, perhaps we may forgot to set the name property to what we get from request
dot body dot name. So by enforcing
validation in mangos, we can ensure that programming
errors like this won't result in invalid
documents to be persisted in a
Mongo DV database. Next, we are going
to look at the built in valid errors in mangos.
3. Utilizing Built-in Mongoose Validators: In the last lecture, we learned about this required validator, which is one of the built
in validators in mangos. In this lecture, we
are going to have a closer look at these
built in validators. So this required property here, we can set that to a boolean or a function that
returns a boolean. And this is useful
when you want to conditionally make your
property required or not. For example, let's
imagine price is only required if the
course is published. Let's add the required
validator here. First, we replace
number with an object. Set the type here
back to number. And then set required. Here, we need to
pass a function. So function. And
in this function, we return a boolean. So we return this to reference this course object
dot is published. So if it's published is true, then price will be required. Okay. Now here, I need
to clarify something. In this particular
case, we cannot replace this function
with an arrow function. In other words, if we do this, our validator will
not work because arrow functions don't
have their own this. They use this value of the
enclosing execution context. In this particular case,
there is a function somewhere in mongoose that is going to call this function. This reference we have here
will reference that function, not the course object
we're dealing with here. So we need to revert this
back to a regular function. Now, let's test this validation. So here's our course object. I'm going to remove the price, and you can see the
course is published. So we should get two
validation errors. Now, back in the terminal, let's run the application. Look, path price is required and path name
is also required. Now later, I will
show you how to get individual error messages
from this exception. For now, we are just getting the message as a simple string. So this is our
required validator. We can set that to
a simple boolean or a function to conditionally
make a property required. Now, depending on the type
of properties we have here, we have additional
built in validators. For example, with strings, we also have Min length and
max length. Let me show you. I'm going to break this down. Here we add min length. Let's say we want to make sure at least we have
five characters. Here we can also set max length. Let's say 255 characters. We also have MT, and here we can pass
a regular expression. Now in this particular case, it doesn't make
any sense to apply a regular expression on
the name of a course. So I'm going to
commend this out. Another useful validator we
have for strings is Enum. I'm going to create
another property here. Let's call that category. And set the type two string. Now here, we can use
the Enum validator. We set this to an array
of valid strings. Let's say we have a few
predefined categories, web mobile network, and so on. So when creating a course, the category we set should
be one of these values. Otherwise, we are going to
get a validation error. So let me make this required. Now back to our course object. Let's add the category, and I'm going to set
this to just attach. Now let's bring the name
back as well as the price. So we can only see the validation
error for the category. So back in the terminal
node, indexed or chase, look, category is not a valid enum value
for path category. So these are the validators
specific to strings. We have Min Land, max length, match for using a regular
expression, and num. For numbers, we
have min and max. So here, price is a number. We can set a minimum of
$10 and a maximum of $200. And we also have these
two validators for dates. In the next lecture,
you are going to learn about
custom validators.
4. Creating Custom Validators in Mongoose: Sometimes the built
in validators in mango don't give us the
kind of validation we need. For example, look at
this tags property. Our tag is a string array. What if we want to
enforce this rule that every core should
have at least one tag? We cannot use the
required validator because with required, we can simply pass
an empty array, and that will be perfectly valid for Mangus' point of view. So here we need a
custom validator. So first, we need to
replace this with an object to replace
this with an object. Here we set the type to array. Now we need to define
a custom validator. So here we set the valid
property to an object. In this object, we have a
property called validator, which we set to a function. This function takes an argument, which is short for value. And here we can implement
our custom validation logic. So we can return
something like this. If Galen is greater than zero, then this property
will be valid. We can also set a
custom message here. So this valid object has another property
that is message. So message, we set that too. A core should have
at least one tag. Now, let's test this. So back in our course
object, first, I'm going to set category
to a valid value, so web, then I'm going
to pass an mt array. So back in the terminal
node, indexed or has. All right, look, a core
should have at least one dig. What if we exclude this
property like this? Let's see what happens. One more time, we get
the same message. A core should have
at least one dig. So if we don't set this property because we define its
type as an array, mongoose will initialize
this to an empty array. Now, what if we
set this to null? So now back in the terminal. Okay, look, cannot read
property length of nub. This is not the kind of validation message
we want to get. So we need to modify our validation logic to
something like this. If we as a value and the length property
is greater than zero, and this property will be valid. So back in the terminal,
let's run this one more time. A core should have at
least one tag, beautiful. So this is how you define
a custom validator. You set the validate
property to an object. In this object, you add
this validator function, and optimally, you
can set a message.
5. Handling Validation Errors in Mongoose: So far, we have only displayed a simple message about
our validation error. In this lecture, we are going to examine this error
object in more detail. So this exception that we get in the cache block has a
property called errors. In this object, we have
a separate property for each invalid property
in our course object. Let me show you what I mean. So back in our course object, here are the
properties of ports. Right now, here we have an
invalid property that is tag. Let's also make category
an invalid property. I'm going to set this to a dash. Now with this errors object that we get will
have two properties. One is tags, the
other is category. Okay? So we can iterate
for all the properties in this error object and get more details about
each validation error. So for field in
error that errors, here we do a console log. We go to R t errors, find that property,
get its value. Now, this is a
validation error object. Let's have a look. So back in
terminal node index dot Js, so let me scroll up and see
what is happening here. All right. So look here, we have a validation
error object. This is the message. Below that, we have the stack trace, okay? Now, all that aside, these are the properties we have in the validation
error option. So here we have a property
called properties, which gives us information about the validation requirements
for this property. So here we have access to
our validator function. You can see the type of
this validator is Enum. These are the valid enum
values for this property. A determines the name
of our property, in this case, category, and value is the current value. So our validation error object has properties and a
few other properties. One is kind, which
is set to Enum, and this is basically a
shortcut to properties type. We also have another
short property path, which is set to
category and value, which is the current
value for this property. So here we are iterating over these validation
error objects. And here we have multiple
validation errors. So this is the first one. And below that, look, we have another
validation error object. This is for a core should
have at least one tag. So if you scroll
down, you can see the kind of this
validation error is user defined because here
we have a custom validator. The path is tags, the
current value is null. So if you want to get the
validation error message for each invalid property, we can simply access
this message property. Now, back in the terminal, let's run the application again. So we have two validation
error messages. Dash is not a valid enum
value for path category, and here is the second
validation error for our text property.
6. Schema Type Options and Customization in Mongoose: So when defining a schema, you have learned that
we can set the type of a property directly here or
use a schema type object. Now, this object has
a few properties. You have learned
about some of them. You know the type property, you know the required
enum, and so on. In this lecture, we
are going to look at a few more useful
properties that are available on these
chema type objects. So for strings, we have three additional properties
that you can use. We have lowercase. We can set that to true. And with this, mongoose
will automatically convert the value of this
category property to lowercase. Let me show you how that works. Back in our course object. Okay, first, I'm going to
remove this validation error. So let's change
category to web and note that here I'm using
an uppercase W, right? I'm going to set
tags to, let's say, nt en now back here in the terminals
from the application. Okay, we created a course object and saved it to the database. Now look at the category. It's lowercase web. And if you look compass, let's refresh this list. So here's our new document. Category is set to
a lowercase web. So this is how the
lowercase property works. We also have uppercase. Again, we can set that to true. Now, technically, we should use one of these,
not both of them. And finally, we have trim. So if we have paddings
around our string, mongoose will automatically
remove those paddings. So these three properties are available when using strings. Now we have a couple
of more properties in the schema type object, and these properties
can be used when defining any property
irrespective of its type. For example, let's go back
to our price property. Let's say we always want to
round the value of the price, so we can define a custom
getter and a custom setter. So get here we pass an arrow function that takes
V or value as an argument. Now we can define our custom
logic or getting this value, so we can apply math dot
round round this value. Now we can similarly
define a custom set. And here we pass a
similar function. So we go to math dot round of. So whenever we set
the price property, the set of function
will be called, and here we will
round that value. So with this, if we go back to our course object and
set the price to 15.8, let's see what happens. So back in the terminal, let's
run the application again. Look, we created a
new coarse object, and the price is set to 16. So here, when we set this value our custom
setter was called. And here we rounded this value. Now back in Compass, here's our last course document. You can see the
price is set to 16. Now let's edit this. You can see the type of
this property is set to Int 32, which is an integer. I'm going to change this to double and then change
the value 16-15 0.8. And finally, click Update
to commit my changes. So here, I'm simulating
a scenario where we have a document that was stored in the database before we
implemented this rounding logic. In this case, if you read these courses and then
access the price property, our custom getter
will be called. And here, we'll
round that value. Let me show you how that works. Let's go back to our
get courses function. Previously, we implemented
this paging logic in the demo. We don't need that. So I'm going to comment out
these two lines, and then I'm going to change the query object so we can
read the particular course. So we want to get
the course with ID set to copy the value
of that course ID. So back in Compass,
here's our course ID. Copy this and paste it here. Here, we're going to get one course so we can access the first
element of this array. Now if you read the
price property, you will see that the value of this property
will be rounded. So here, let's read
the price property. Let's come into this create
course and call get courses. Back in terminal. Let's
run the application. Okay, look, price is 16, even though in the database, we stored it as 15.8. So that's how these custom
getters and setters work. The setter is called when we set the value of a
property like here, and the getter is called when we read the
value of a property.
7. Restructuring our FareWheels Project: All right. So here's our
fair wheels application. Now, if you look at
the customer's module, you can see here on the top, we are defining this
customer model. And below that, we have
our route handlers. Then after all these
route handlers, we have this validate
customer function. So this is a fairly
simple application. And in this module, we
have 85 lines of code. If you look at the definition of the customer object or
the customer model, this is not a big complex model. In a real world application, our customer model is
going to be more complex. So the code in this
module is going to grow, and that's something we need
to address in this lecture. To keep our applications
maintainable, we should ensure
that each module is responsible for
only one thing. That is the single responsibility
principle in practice. In this application,
this customer's module that we have is part
of the routes folder. So technically, all
we should have in this module is a definition
of our customer's route. The definition of
a customer object doesn't really belong
in this module. So in this lecture,
we're going to extract this code and
put it somewhere else. So I've created
this models folder. In this folder, we are going to have modules like customer Dogs, company dogs, and so on. So let's add a new
filer, customer dot js. Now back in customers dot JS, I'm going to move
the definition of the customer model
inside our new model. So let's move that here. Now here, we have a
dependency to mangos. So let's go back
here on the top. This is the line
for loading mangos. We also need to load I, as you will see in a second. Now back in our
customers module, I'm also going to
move the function for validating a customer
inside our new module. So and paste it here at the end. Now we have the single responsibility
principle in practice. Our customer module
has all the code for defining and validating
a customer object. It knows what a customer
should look like. Our customers JS module knows all about various routes
to work with customers. So here we don't have any code other than handling
express routes. Okay? That means we no longer need to load
joy in this module because validating a
customer object is now the responsibility of this
new module, customer dot JS. Okay? Now, finally, at
the end of this module, we need to export
this customer class as well as this validate
customer function. So we write module dot exports. We add customer here
in this object or a shorter way is to simply
use the export property. Earlier, I told you that exports is reference
to module dot exports. So we can simply add extra
properties in that object. Similarly, we need to export
this validate function. We can make the name shorter. So instead of validate customer, we can use validate and we set
this to validate customer. Okay? Now back in
our old module, here we have two choices. One is to load the
customer module like this. So constant customer,
we set this to require. Now here, we need to go
one level up and then go to the models folder and then load the
customer module. So this customer module, this object has two properties. One is customer, the
other is validate. If we load this customer
module like this, in order to reference
the customer type or the customer model, we have to write customer
module dot customer. And you can see this
looks really ugly. So a better approach is to
use object destructuring. This object that is returned
from loading this module, you know that it
has two properties, customer and validate. We can destructure that
object and load it into these two constants,
customer and validate. So we put the curly braces here when defining
these constants. And with this, the
customer constant will be set to what is
returned from this module. Mar. Okay. So we don't have to repeat
customer in several places. Similarly, this validate
property will be set to what is returned
from this module, validate. Now finally, we need to replace this validate customer
with palidate which is a shorter name and cleaner here's another reference
that we need to update. So validate. Now
with this change, if you look at the
number of lines of code we have in this module, look, we have 54 lines. So initially, we had
over 80 lines of code, and now we have about
50 lines of code. As an exercise, I want you to modify
the company's module. So in routes, here
we have companies. Similarly, here on the top, we have the definition
of the company model. I want you to extract this code and put it
in a separate module.