Skip to main content

Intro To Mongoose

Learning Objectives

Students Will Be Able To:
Describe the Use Case for Mongoose
Define a Basic Schema for a Model
Connect to a MongoDB Using Mongoose
Create Documents Using a Model
Read Documents Using a Model
Define Default Values in a Schema
Define Validations in a Schema

Road Map

  1. Demo of the mongoose-movies Reference Application
  2. Setup
  3. Intro to Mongoose
  4. Using a .env File to Protect "Secrets"
  5. Including Mongoose in a Project
  6. Defining Schemas in Mongoose
  7. Built-in Types for Properties
  8. Compiling Schemas into Models
  9. Use a Model to Create Data in a Node REPL
  10. Review the 5-Step Process to Add a Feature to a Web App
  11. Implement the new Functionality
  12. Implement the create Functionality
  13. Use a Model to Read Data
  14. Implement index Functionality
  15. Further Study
    • Defining Default Values for a Property
    • Defining Validations for a Property

1. Demo of the mongoose-movies Reference Application

As we continue to learn new development concepts and techniques during this unit we'll be incorporating them in a reference app named Mongoose Movies.

In fact, we'll be building a reference app for each full-stack unit that can be referred to when coding your labs and project.

2. Setup

We'll be learning about Mongoose over several lessons so a code-along repo has been set up to get started and to sync with if need be.

To get set up:

  1. Move into your code folder:

    cd ~/code
SKIP cloning the repo
  1. Clone the repo containing the starter code:

    git clone https://git.generalassemb.ly/sei-blended-learning/mongoose-movies.git
  1. Create a new express-generator app

    npx express-generator --no-view mongoose-movies
  2. Move into the newly created project mongoose-movies folder:

    cd mongoose-movies
  3. Install the Node modules:

    npm i
  4. Open the project in VS Code:

    code .
  5. Change app.js to server.js for best practices

    mv app.js server.js

    Update the imports in bin/www

    var app = require('../server');
  6. Open an integrated terminal in VS Code (control + backtick) and spin up the server:

    nodemon
SKIP browsing to localhost

Browsing to localhost:3000 we find that the starter code is a skeleton Express server created using express-generator and updated as follows:

  • As a best practice, app.js has been renamed to server.js
  • Partial templates (header.ejs & footer.ejs) have been set up

3. Intro to Mongoose

What is Mongoose?

Yes, this guy, but not in the context of MongoDB...

Mongoose is software library called an Object Document Mapper (ODM) because it maps object-oriented JavaScript to MongoDB documents.

Why use Mongoose?

Mongoose is the goto when performing CRUD data operations on a MongoDB database.

Using Mongoose is easier and more productive than working directly MongoDB.

As stated on Mongoose's homepage:

info

"Mongoose provides a straight-forward, schema-based solution to model your application data..."_

Wait a minute, what's with this "schema" business, isn't MongoDB schema-less?

Well, yes MongoDB is, however, it turns out that the vast majority of applications benefit when their data conforms to a defined structure (schema).

In addition to ensuring that data conforms to schemas that we define Mongoose also provides lots of other useful functionality such as:

  • Default property values
  • Data validation
  • Automatic related model population via the populate method
  • Virtual properties - create properties like "fullName" that are not persisted in the database
  • Custom Instance methods which operate on a document
  • Custom Static methods which operate on the entire collection
  • pre and post event lifecycle hooks (Mongoose "middleware")

The Big Picture

Here is a big picture overview of the purpose of Mongoose's Schema and Model components:

Big Picture Example

Assume we need to store cat documents with the following schema:

models/cat.js
const catSchema = new mongoose.Schema({
name: String,
breed: String
});

catSchema can then be compiled into a Model and that Model exported like this:

// By convention, the name of the Model is singular and UpperCamelCased
module.exports = mongoose.model('Cat', catSchema);

Then, in our controller actions, we can require the Cat model and use it to perform CRUD on the cats collection in the MongoDB:

// Again, convention is to name the Model's
// variable as singular and UpperCamelCased
const Cat = require('./models/cat');
// SKIP old code
// Cat.create({name: 'Morris', breed: 'Orange Tabby'}, function(err, catDoc) {
// console.log(catDoc);
// });
const kitty = new Cat({name: 'Morris', breed: 'Orange Tabby'});
kitty.save().then(() => console.log('meow'));

❓ Review Questions

(1) Describe the use case for Mongoose, i.e., what is it and why choose to use it?


Mongoose is a JavaScript library known as an Object Document Mapper (ODM) and we'll want to use it any time a project is CRUDing a MongoDB database.


(2) A Mongoose _________ is compiled into a Mongoose Model.


Schema


(3) We use a Mongoose _________ to perform CRUD operations on a particular MongoDB collection.


Model


4. Using a .env File to Protect "Secrets"

A .env file is used to provide environment variables (also called config vars).

Environment variables include the database's connection string and other secrets such as access tokens, etc.

caution

Because they usually hold sensitive info, .env files should never be pushed to GitHub and thus need to be Git ignored.

Environment variables can also be used to configure an application's settings without changing the app's source code.

Setting Up the .env

First, create a .env file in the project root:

touch .env

Install the dotenv Node module:

npm i dotenv

In server.js, require dotenv and call its config() method to "process" the KEY=VALUE pairs in the .env:

server.js
const logger = require('morgan');

// It's very important to require dotenv before any other module
// that depends upon the properties added to process.env
require('dotenv').config();
note

Note that we're not assigning the returned value to a variable - just simply coding require('dotenv').config() results in the .env file being "processed".

Adding the Atlas MongoDB Connection String to the .env

Find and copy your Atlas MongoDB connection string that you previously created and tucked away for safe keeping.

In the .env file, add a key of DATABASE_URL followed by an equals sign (=) then paste in your connection string with no spaces resulting in something like this:

DATABASE_URL=mongodb+srv://someuser:somepassword@cluster0-oxpsb.azure.mongodb.net/?retryWrites=true&w=majority
note

Note that every KEY=VALUE pair defined in .env will be available as a property on Node's process.env object, e.g., process.env.DATABASE_URL.

Next we need to insert the name of the database we want to create and connect to, mongoose-movies will work just fine for this project. Insert it between the /? characters like this:

DATABASE_URL=mongodb+srv://someuser:somepassword@cluster0-oxpsb.azure.mongodb.net/mongoose-movies?retryWrites=true&w=majority
tip

👀 This might be a good time to update your saved connection string in its new form for future use. Just be sure to change the database name, e.g., change /mongoose-movies? to to /another-db-name? in your next project.

caution

FYI, nodemon doesn't watch for changes in the .env file, so you may have to restart the server manually if you're not going to make some additional code changes.

5. Including Mongoose in a Project

To use Mongoose in our apps, we need to install and configure it...

Install Mongoose

Installing the Mongoose module is straight forward:

npm i mongoose
info

Reminder: npm i is a shortcut for npm install

Connect to MongoDB using Mongoose in a module

We're going to create a separate module named database.js and put it in a folder named config:

mkdir config
touch config/database.js

Then in database.js, let's connect to a database named movies:

database.js
const mongoose = require('mongoose');

mongoose.set("debug", true);
mongoose.connect(process.env.DATABASE_URL);

As you know, the code in a Node module doesn't execute until the module is required, therefore, to connect to the database, we'll require database.js in server.js:

server.js
const logger = require('morgan');

require('dotenv').config();
// connect to the database with AFTER the config vars are processed
require('./config/database');

Again, there's no need to assign to a variable because we're not exporting anything useful.

Verify the Connection

Errors in the Express terminal - look closely, it's probably a failure to authenticate with Alas error - please double-check your connection string and its credentials.

No errors in the Express terminal? Great! However, wouldn't it be nice to know that our connection to our database was successful? Sure it would...

Adding event listeners to the Mongoose connection

The Mongoose connection object inherits from Node's EventEmitter which allows us to listen to defined events.

Let's listen to the connected event by modifying the database.js module as follows:

database.js
const mongoose = require('mongoose');

mongoose.set("debug", true);
mongoose.connect(process.env.DATABASE_URL);

// shortcut to mongoose.connection object
const db = mongoose.connection;

db.on('connected', function() {
console.log(`Connected to MongoDB ${db.name} at ${db.host}:${db.port}`);
});

After saving, nodemon will restart the server and you should see the Connected to MongoDb... message in the Express terminal.

👀 Do you need to sync your code?


git reset --hard origin/sync-1-connect-database


6. Defining Schemas in Mongoose

Next, we're going to:

  1. Create a module for the Schema/Model
  2. Define a basic Schema for a Movie model

Create a module for the Schema/Model

So, where are we going to put our app's schemas and models?

The MVC design pattern is all about code organization and it makes sense to organize modules that define schemas/models within a models folder:

mkdir models
touch models/movie.js

There will always be a module per Mongoose Model where:

  1. We define the schema,
  2. Compile the schema into a model, and
  3. Export that model.

Define a basic Schema for a Movie model

In the schema/model module, we will start with this code:

models/movie.js
const mongoose = require('mongoose');
// optional shortcut to the mongoose.Schema class
const Schema = mongoose.Schema;

Creating the shortcut to the mongoose.Schema class is optional but convenient when defining most schemas.

Now let's define the basic schema for the Movie Model:

models/movie.js
const Schema = mongoose.Schema;

const movieSchema = new Schema({
title: String,
releaseYear: Number,
mpaaRating: String,
cast: [String]
});

Note the cast property's type is an Array of Strings.

info

Mongoose vocabulary: A property may also be referred to as a "path".

👉 YOU DO - Define Another Schema Property (1 min)

  1. Add an additional property named nowShowing with a type of Boolean (make sure that it's upper-cased so that it refers to JavaScript's built-in Boolean object wrapper).

Try not to peek...


const movieSchema = new Schema({
title: String,
releaseYear: Number,
mpaaRating: String,
cast: [String],
// Newly added property below
nowShowing: Boolean
});

Add the timestamps Option

Mongoose can automatically create createdAt and updatedAt fields to every document if we provide a second argument to the Schema constructor and set a timestamps property in it as follows:

models/movie.js
const movieSchema = new mongoose.Schema({
...
}, {
timestamps: true
});

This really comes in handy so it's recommended to pretty much add the timestamps: true option to all schemas that you define.

Awesome! We've defined a Mongoose schema!

As we progress toward learning more about Mongoose, we will be adding additional functionality to the movieSchema.

7. Built-in Types for Properties

Here are the eight built-in schemaTypes available:

  • String
  • Number
  • Boolean
  • Date
  • mongoose.Schema.Types.ObjectId
  • mongoose.Schema.Types.Buffer
  • [] (Array)
  • mongoose.Schema.Types.Mixed

Of note are a few types above that are not built into JavaScript:

  • mongoose.Schema.Types.ObjectId
  • mongoose.Schema.Types.Buffer
  • mongoose.Schema.Types.Mixed

When we need to specify one of the above types, e.g., ObjectId, we will need to ensure that we access them through the object hierarchy.

Defining that Schema shortcut variable, enables us to write Schema.Types.ObjectId, leaving off the mongoose..

8. Compiling Schemas into Models

caution

Reminder: Models, not schemas are used to perform CRUD on a MongoDB collection.

Compiling a schema into a model is as easy as calling the mongoose.model method:

models/movie.js
const Schema = mongoose.Schema;

const movieSchema = new Schema({
title: String,
releaseYear: Number,
mpaaRating: String,
cast: [String],
nowShowing: Boolean
}, {
timestamps: true
});

// Compile the schema into a model and export it
module.exports = mongoose.model('Movie', movieSchema);

Again, there is a one-to-one mapping between Mongoose models and MongoDB collections.

By default, the collection in the database will be named as the pluralized version of the model in all lower-case.

Congrats on defining your first Mongoose schema and exporting its model!

👀 Do you need to sync your code?


git reset --hard origin/sync-2-create-model


9. Use a Model to Create Data in a Node REPL

Now that we have a model, we're ready to perform some CRUD.

First up is creating data!

We can use a Mongoose Model in two ways to create documents in the collection:

  1. const instance = new Model(<object>), theninstance.save()
    -OR-

  2. Model.create(<object or array of objects>)

Creating documents in a Node REPL

Before writing the code in the app to create movies, let's do so in a Node REPL...

Warning, if you make a typo, you'll have to start over:

node
> require('dotenv').config() // Necessary if connection string in a .env file
> require('./config/database')
> const Movie = require('./models/movie')
> Movie.create({
... title: 'Star Wars - A New Hope',
... releaseYear: 1977
... }).then(console.log)

Pretty neat how we can provide the console.log method as the onFulfilled callback function.

Logged out will be a document that looks something like...

{ __v: 0,
title: 'Star Wars - A New Hope',
releaseYear: 1977,
_id: 57ea692bab09506a97e969ba,
cast: []
}
note

Note: The __v field is added by Mongoose to track versioning - ignore it.

Note that we did not provide a value for nowShowing so it was not created as a property in the document.

However, properties of type Array, are always initialized to empty arrays like cast was. This makes it convenient to start pushing performers into it!

That was fun! Exit the REPL (ctrl + C twice) and lets see how we can use
new + save
to create movie documents - but this time from within our app...

10. Review the 5-Step Process to Add a Feature to a Web App

Once again, here is the process we repeat over and over when adding a feature, including CRUD functionality, in a web app:

  1. Determine the "proper" route (HTTP Method & Endpoint). Use RESTful conventions whenever possible.
  2. Add the UI (link and/or form) that will trigger the HTTP request that matches the route.
  3. Define the route in the appropriate router module that will match the HTTP request and map it to the <controller>.<method> that will perform the desired functionality.
  4. Add and code the controller action/function to perform any necessary CRUD, etc. and be sure to export it.
  5. In the controller, in the case of a GET request, respond with res.render (optionally passing data to the view). Or, when data has been mutated (POST, PUT & DELETE) use a res.redirect. If rendering, code the view template if necessary.
SKIP implementing the new functionality

11. Implement the new Functionality

Creating Data is sometimes a two-request process

Remember, creating data functionality might be a two-request process:

  1. First request displays a form for the user to enter the data (the new functionality/controller action)
  2. Second request to submit the form to the server where the data is created (the create functionality/controller action)

Yup, the new & create actions are partners!

❓ What other two controller actions are partners used to edit data?


edit + update


Displaying a <form> to enter a new movie

Step 1 - Determine the "proper" route (HTTP Method & Endpoint)

Using our trusty Routing Chart, we find that to display a new.ejs view with a form for entering movies, the proper route will be:

GET /movies/new

Step 2 - Add the UI

👉 YOU DO - Add a Navigation Hyperlink (1 min)

  1. Inside of the existing <nav> element in views/partials/header.ejs, add a hyperlink (<a> tag) that when clicked will result in the browser sending the HTTP request for the route we just identified above.

  2. The hyperlink should display New Movie like this:

    👀 Note that a &nbsp;|&nbsp; has been inserted between the links to provide separation.

Step 3 - Define the route on the server

Express generator stubbed up a users.js route module that we will convert and use for the movies resource.

Rename the file to movies.js.

Due to the renaming, we'll need to make a couple of changes in server.js...

❓ What two lines of code need to be updated?


var usersRouter = require('./routes/users');
and
app.use('/users', usersRouter);


Inside of routes/movies.js, let's code the route that maps the HTTP request to the new action:

routes/movies.js
const express = require('express');
const router = express.Router();
// You'll be creating this controller module next
const moviesCtrl = require('../controllers/movies');

// GET /movies/new
router.get('/new', moviesCtrl.new);

module.exports = router;

Step 4 - Add the controller action/method and be sure to export it

👉 YOU DO - Create the movies Controller (3 mins)

Create the controller module, export the new action and stub up the newMovie function - we've done this quite a few times already so do your best not to look below...

  1. Create the controllers/movies.js module.

  2. The new action is just the first of several that are going to be exported from this module:

controllers/movies.js
module.exports = {
new: newMovie
};

Remember, we can't name a function new because it's a reserved word, but newMovie works!

  1. Stub up the newMovie function:
controllers/movies.js
function newMovie(req, res) {

}

Code the controller action/method

There's no CRUD to perform in this new action, we just need to render a new.ejs:

function newMovie(req, res) {
// We'll want to be able to render an
// errorMsg if the create action fails
res.render('movies/new', { errorMsg: '' });
}

Step 5 - Create the View

Now we need the new.ejs view.

As we've discussed, organizing views for a data resource into a dedicated folder makes sense. In Terminal:

mkdir views/movies
touch views/movies/new.ejs

Copy and paste the following awesome but ugly form, then we'll review it...

views/movies/new.ejs
<%- include('../partials/header') %>
<h2>Enter a New Movie</h2>
<form action="/movies" method="POST">
<label>Title:
<input type="text" name="title">
</label><br />
<label>Release Year:
<input type="number" name="releaseYear">
</label><br />
<label>MPAA Rating
<select name="mpaaRating">
<option value="G">G</option>
<option value="PG">PG</option>
<option value="PG-13">PG-13</option>
<option value="R">R</option>
</select>
</label><br />
<label>Cast (separate actors with commas):
<input type="text" name="cast">
</label><br />
<label>Now Showing:
<input type="checkbox" name="nowShowing" checked>
</label><br />
<button type="submit">Add Movie</button>
</form>
<p><%= errorMsg %></p>

<%- include('../partials/footer') %>

Test the new Functionality

Clicking the New Movie link should now display

👀 Do you need to sync your code?


git reset --hard origin/sync-3-new-functionality


12. Implement the create Functionality

Here we go again...

Step 1 - Determine the "proper" route (HTTP Method & Endpoint)

Note that we've already set the action & method attributes to match the proper RESTful route to submit the form to.

We are going to POST to /movies with the following JSON body:

{
"title": "SEI Movie",
"releaseYear": 2023,
"mpaaRating": "PG",
"nowShowing": "on",
"cast": "Simon Desmond",
}
note

We can send any JSON body that we want, but the above mirrors what a normal HTML form will send

Identify the HTTP Method and Endpoint

SKIP creating the UI - Use Insomnia to send the request

Step 2 - Add the UI

✅ Yep, we already have the <form> that is going to trigger the request.

Step 3 - Define the route on the server

Define the create route in routes/movies.js:

routes/movies.js
// GET /movies/new
router.get('/new', moviesCtrl.new);
// POST /movies
router.post('/', moviesCtrl.create);

Step 4 - Add and code the controller action/method and be sure to export it

The next step is to stub up and export that create controller action in controllers/movies.js:

controllers/movies.js
module.exports = {
new: newMovie,
create
};

async function create(req, res) {

}

👀 We're going to be mostly using the modern async/await approach when consuming promises vs. using .then() because, well, async/await simply rock!

We're going to be using our Movie model, so we need to require it at the top:

controllers/movies.js
const Movie = require('../models/movie');

Now let's write the code that will use the Movie Model to create the movie submitted by the form - we'll review it as we type it...

controllers/movies.js
async function create(req, res) {
// convert nowShowing's checkbox of nothing or "on" to boolean
req.body.nowShowing = !!req.body.nowShowing;
// remove any whitespace at start and end of cast
req.body.cast = req.body.cast.trim();
// split cast into an array if it's not an empty string - using a regular expression as a separator
if (req.body.cast) req.body.cast = req.body.cast.split(/\s*,\s*/);
try {
const movie = await Movie.create(req.body);
// Always redirect after CUDing data
// We'll refactor to redirect to the movies index after we implement it
// res.redirect('/movies/new'); SKIP old code
res.status(201).json(movie)
} catch (err) {
// Typically some sort of validation error
console.log(err);
// res.render('movies/new', { errorMsg: err.message }); SKIP old code
res.status(500).json({ err })
}
}

Step 5 - Redirect

We've already coded the redirect above and have no view to code.

Test the create Functionality

You should now be able to submit movies - congrats!

Now that we've created a movie or two, let's see how we use Mongoose models to read documents from a MongoDB collection...

👀 Do you need to sync your code?


git reset --hard origin/sync-4-create-functionality


13. Use a Model to Read Data

The querying ability of Mongoose is very capable. For example:

const movies = await Movie.find({ mpaaRating: 'PG' })
.where('releaseYear').lt(1970)
.where('cast').in('Bob Hope')
.sort('-title')
.limit(3)
.select('title releaseYear');

The above query builder syntax is unique to Mongoose and is not available in MongoDB.

Powerful? Yes, but we're going to start with the basics!

Useful Model methods to query for data

Here are the common Model methods for querying data:

MethodPurposeSyntax
findReturns an array of all documents matching the query objectMovie.find({mpaaRating: 'PG'}, function(err, movies) {...})
findByIdFind a document based on it's _idMovie.findById(req.params.id, function(err, movie) {...})
findOneFind the first document that matches the query objectMovie.findOne({releaseYear: 2000}, function(err, movie) {...})

Note: An empty query object, {}, selects ALL documents.

14. Implement index Functionality

If you're feeling up to it, pause the video and give the following exercise that implements the movies index functionality a go.

However, it's fully understandable if you'd like to follow along with me! 💪 Practice Exercise - Implement the movies index functionality that displays the list of movies (20 mins)

  1. Identify the RESTful route
  1. Use Insonmnia to trigger the request

  2. Define the RESTful route

  3. Stub up and export the movie controller's index action.

  4. Code the index action to:

    • Use the Movie model to query for all movies. As mentioned above, use an empty query object to retrieve all documents.
SKIP creating the UI
  1. Create a views/movies/index.ejs view to display in an HTML table. Here's most of the markup - please complete according to the comments:

    views/movies/index.ejs
    <%- include('../partials/header') %>

    <h2>Movie List</h2>
    <table>
    <thead>
    <tr>
    <th>Title</th>
    <th>Release Year</th>
    <th>Rating</th>
    <th>Cast</th>
    <th>Now Showing</th>
    </tr>
    </thead>
    <tbody>
    <!-- Write the line of EJS to iterate over movies using forEach -->
    <tr>
    <td><%= m.title %></td>
    <td><%= m.releaseYear %></td>
    <td><%= m.mpaaRating %></td>
    <td><%= m.cast.join(', ') %></td>
    <!-- finish the ternary expression to display 'Yes' or 'Nope' -->
    <td><%= m.nowShowing ? %></td>
    </tr>
    <!-- Close the forEach using EJS here -->
    </tbody>
    </table>

    <%- include('../partials/header') %>

Now a quick refactor...

Refactor the create Action's Redirect

Now that we have an index view, let's update the redirect in the create action:

controllers/movie.js
async function create() {
...
try {
await Movie.create(req.body);
// Always redirect after CUDing data
// We'll refactor to redirect to the movies index after we implement it
res.redirect('/movies'); // Update this line
...
}

Congrats on implementing new and create functionality!

👀 Do you need to sync your code?


git reset --hard origin/sync-5-index-functionality


Further Study or Code-Along?

We always encourage practicing expanding your learning on your own by reviewing the Further Study sections.

However, this time, the option is yours, you can stop the video and implement default values and validation on your own, or feel to follow along as I take you through it!

15. Further Study

Defining Default Values for a Property

If a certain property is not provided when creating data, it's possible to specify in the schema a default value to use.

To add a default value, we need to switch from this simple property definition syntax:

const movieSchema = new Schema({
title: String,
releaseYear: Number,
...

To this object syntax:

const movieSchema = new Schema({
title: String,
releaseYear: { type: Number },
...

Now we can add a default key to specify a default value:

const movieSchema = new mongoose.Schema({
title: String,
releaseYear: { type: Number, default: 2000 },
mpaaRating: String,
cast: [String],
nowShowing: { type: Boolean, default: false }
});

Silly example defaulting the release year to 2000 - yes. But that's how we can add a simple default value.

note

Note that defaults for array types will not work - they require the use of Mongoose middleware to set default values.

Test it out in the app and we'll find that it didn't work for the releaseYear because req.body.releaseYear exists (albeit its value is an empty string) and this prevents the default from being assigned.

We can fix this in the create action by deleting any property in req.body that is an empty string:

if (req.body.cast) req.body.cast = req.body.cast.split(/\s*,\s*/);
// Remove empty properties so that defaults will be applied
for (let key in req.body) {
if (req.body[key] === '') delete req.body[key];
}

Now if we don't type in a value in the form for the releaseYear property, the default of 2000 will be set.

Using a function to provide a default value

You've seen how to add a simple default value, but we can also provide a function as well.

The property's default would then be set to the value returned by the function!

For example, we can take our silly default for releaseYear and make it default to the current year like this:

models/movie.js
const movieSchema = new mongoose.Schema({
title: String,
releaseYear: {
type: Number,
default: function() {
return new Date().getFullYear();
}
},
mpaaRating: String,
cast: [String],
nowShowing: { type: Boolean, default: true }
}, {
timestamps: true
});

Defining Validations for a Property

Validations are used to prevent bogus data, i.e., data that does not conform to the schema, from being saved in the database.

There are several built-in validators we can use.

However, endless flexibility is possible with custom asynchronous and synchronous validator functions and/or Mongoose middleware.

As always, let's take a look at the basics at first...

Movies should not be allowed to be created without a title. Let's make it required:

models/movie.js
const movieSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
...

Now, if we try saving a movie without a title a validation error will occur and the data will not be saved.

👀 As a reminder, we coded the create action to render an error message in the new.ejs view if an error occurs.

For properties that are of type Number, we can specify a min and max value:

const movieSchema = new mongoose.Schema({
...
releaseYear: {
type: Number,
default: function() {
return new Date().getFullYear();
},
min: 1927
},
...

Cool - no more silent movies!

For properties that are of type String, we have:

  • enum: String must be in the provided list
  • match: String must match the provided regular expression
  • maxlength and minlength: Take a guess :)

Here is how we use the enum validator on the mpaaRating property:

const movieSchema = new mongoose.Schema({
...
mpaaRating: {
type: String,
enum: ['G', 'PG', 'PG-13', 'R']
},
...
note

Note: Some of you may think the above validation is unnecessary because the user is restricted by the choices in the form's <select>, however, it's quite easy to bypass such client-side validation - so, it's always a good idea to validate on the server.

👀 Do you need to sync your code?


git reset --hard origin/sync-6-finish-intro


References