Lesson 4: Introduction to Mongoose

Mongoose is a robust Node.js ODM module that adds MongoDB support to your Express application. Mongoose uses schemas to model your entities, offers predefined validation along with custom validations, allows you to define virtual attributes, and uses middleware hooks to intercept operations. The Mongoose design goal is to bridge the gap between the MongoDB schemaless approach and the requirements of real-world application development. In this Lesson, you'll go through the following basic features of Mongoose:

  • Mongoose schemas and models
  • Schema indexes, modifiers, and virtual attributes
  • Using the model's methods and perform CRUD operations
  • Verifying your data using predefined and custom validators
  • Using middleware to intercept the model's methods

Introducing Mongoose

Mongoose is a Node.js module that provides developers with the ability to model objects and save them as MongoDB documents. While MongoDB is a schemaless database, Mongoose offers you the opportunity to enjoy both strict and loose schema approaches when dealing with Mongoose models. Like with any other Node.js module, before you can start using it in your application, you will first need to install it. The examples in this Lesson will continue directly from those in the previous Lessons; so for this Lesson, copy the final example from Lesson 2, Building an Express Web Application, and let's start from there.

Installing Mongoose

Once you've installed and verified that your MongoDB local instance is running, you'll be able connect it using the Mongoose module. First, you will need to install Mongoose in your application modules folders, so change your package.json file to look like the following code snippet:

{
  "name": "MEAN",
  "version": "0.0.5",
  "dependencies": {
    "express": "~4.8.8",
    "morgan": "~1.3.0",
    "compression": "~1.0.11",
    "body-parser": "~1.8.0",
    "method-override": "~2.2.0",
    "express-session": "~1.7.6",
    "ejs": "~1.0.0",
    "mongoose": "~3.8.15"
  }
}

To install your application dependencies, go to your application folder and issue the following command in your command-line tool:

$ npm install

This will install the latest version of Mongoose in your node_modules folder. After the installation process has successfully finished, the next step will be to connect to your MongoDB instance.

Connecting to MongoDB

To connect to MongoDB, you will need to use the MongoDB connection URI. The MongoDB connection URI is a string URL that tells the MongoDB drivers how to connect to the database instance. The MongoDB URI is usually constructed as follows:

mongodb://username:password@hostname:port/database

Since you're connecting to a local instance, you can skip the username and password and use the following URI:

mongodb://localhost/mean-book

The simplest thing to do is define this connection URI directly in your config/express.js configuration file and use the Mongoose module to connect to the database as follows:

var uri = 'mongodb://localhost/mean-book';
var db = require('mongoose').connect(uri);

However, since you're building a real application, saving the URI directly in the config/express.js file is a bad practice. The proper way to store application variables is to use your enviornment configuration file. Go to your config/env/development.js file and change it to look like the following code snippet:

module.exports = {
  db: 'mongodb://localhost/mean-book',
  sessionSecret: 'developmentSessionSecret'
};

Now in your config folder, create a new file named mongoose.js that contains the following code snippet:

var config = require('./config'),
    mongoose = require('mongoose');

module.exports = function() {
  var db = mongoose.connect(config.db);

  return db;
};

Notice how you required the Mongoose module and connected to the MongoDB instance using the db property of your configuration object. To initialize your Mongoose configuration, go back to your server.js file, and change it to look like the following code snippet:

process.env.NODE_ENV = process.env.NODE_ENV || 'development';

var mongoose = require('./config/mongoose'),
    express = require('./config/express');

var db = mongoose();
var app = express();
app.listen(3000);

module.exports = app;

console.log('Server running at http://localhost:3000/');

That's it, you have installed Mongoose, updated your configuration file, and connected to your MongoDB instance. To start your application, use your command-line tool, and navigate to your application folder to execute the following command:

$ node server

Your application should be running and connected to the MongoDB local instance.

Note

If you experience any problems or get this output: Error: failed to connect to [localhost:27017], make sure your MongoDB instance is running properly.

Understanding Mongoose schemas

Connecting to your MongoDB instance was the first step but the real magic of the Mongoose module is the ability to define a document schema. As you already know, MongoDB uses collections to store multiple documents, which aren't required to have the same structure. However, when dealing with objects, it is sometime necessary for documents to be similar. Mongoose uses a Schema object to define the document list of properties, each with its own type and constraints, to enforce the document structure. After specifying a schema, you will go on to define a Model constructor that you'll use to create instances of MongoDB documents. In this section, you'll learn how to define a user schema and model, and how to use a model instance to create, retrieve, and update user documents.

Creating the user schema and model

To create your first schema, go to the app/models folder and create a new file named user.server.model.js. In this file, paste the following lines of code:

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var UserSchema = new Schema({
  firstName: String,
  lastName: String,
  email: String,
  username: String,
  password: String
});

mongoose.model('User', UserSchema);

In the preceding code snippet, you did two things: first, you defined your UserSchema object using the Schema constructor, and then you used the schema instance to define your User model. Next, you'll learn how to use the User model to perform CRUD operations in your application's logic layer.

Registering the User model

Before you can start using the User model, you will need to include the user.server.model.js file in your Mongoose configuration file in order to register the User model. To do so, change your config/mongoose.js file to look like the following code snippet:

var config = require('./config'),
    mongoose = require('mongoose');

module.exports = function() {
  var db = mongoose.connect(config.db);

  require('../app/models/user.server.model');

  return db;
};

Make sure that your Mongoose configuration file is loaded before any other configuration in the server.js file. This is important since any module that is loaded after this module will be able to use the User model without loading it by itself.

Creating new users using save()

You can start using the User model right away, but to keep things organized, it is better that you create a Users controller that will handle all user-related operations. Under the app/controllers folder, create a new file named users.server.controller.js and paste the following lines of code:

var User = require('mongoose').model('User');

exports.create = function(req, res, next) {
  var user = new User(req.body);

  user.save(function(err) {
    if (err) {
      return next(err);
    } else {
      res.json(user);
    }
  });
};

Let's go over this code. First, you used the Mongoose module to call the model method that will return the User model you previously defined. Next, you create a controller method named create(), which you will later use to create new users. Using the new keyword, the create() method creates a new model instance, which is populated using the request body. Finally, you call the model instance's save() method that either saves the user and outputs the user object, or fail, passing the error to the next middleware.

To test your new controller, let's add a set of user-related routes that call the controller's methods. Begin by creating a file named users.server.routes.js inside the app/routes folder. In this newly created file, paste the following lines of code:

var users = require('../../app/controllers/users.server.controller');

module.exports = function(app) {
  app.route('/users').post(users.create);
};

Since your Express application will serve mainly as a RESTful API for the AngularJS application, it is a best practice to build your routes according to the REST principles. In this case, the proper way to create a new user is to use an HTTP POST request to the base users route as you defined here. Change your config/express.js file to look like the following code snippet:

var config = require('./config'),
    express = require('express'),
    morgan = require('morgan'),
    compress = require('compression'),
    bodyParser = require('body-parser'),
    methodOverride = require('method-override'),
    session = require('express-session');

module.exports = function() {
  var app = express();

  if (process.env.NODE_ENV === 'development') {
    app.use(morgan('dev'));
  } else if (process.env.NODE_ENV === 'production') {
    app.use(compress());
  }

  app.use(bodyParser.urlencoded({
    extended: true
  }));
  app.use(bodyParser.json());
  app.use(methodOverride());

  app.use(session({
    saveUninitialized: true,
    resave: true,
    secret: config.sessionSecret
  }));

  app.set('views', './app/views');
  app.set('view engine', 'ejs');

  require('../app/routes/index.server.routes.js')(app);
  require('../app/routes/users.server.routes.js')(app);

  app.use(express.static('./public'));

  return app;
};

That's it! To test it out, go to your root application folder and execute the following command:

$ node server

Your application should be running. To create a new user, perform an HTTP POST request to the base users route, and make sure the request body includes the following JSON:

{
  "firstName": "First",
  "lastName": "Last",
  "email": "[email protected]",
  "username": "username",
  "password": "password"
}

Another way to test your application would be to execute the following curl command in your command-line tool:

$ curl -X POST -H "Content-Type: application/json" -d '{"firstName":"First", "lastName":"Last","email":"[email protected]","username":"username","password":"password"}' localhost:3000/users

Note

You are going to execute many different HTTP requests to test your application. curl is a useful tool, but there are several other tools specifically designed for this task; we recommend that you find your favorite one and use it from now on.

Finding multiple user documents using find()

The find() method is a model method that retrieves multiple documents stored in the same collection using a query and is a Mongoose implementation of the MongoDB find() collection method. To understand this better, add the following list() method in your app/controllers/users.server.controller.js file:

exports.list = function(req, res, next) {
  User.find({}, function(err, users) {
    if (err) {
      return next(err);
    } else {
      res.json(users);
    }
  });
};

Notice how the new list() method uses the find() method to retrieve an array of all the documents in the users collection. To use the new method you created, you'll need to register a route for it, so go to your app/routes/users.server.routes.js file and change it to look like the following code snippet:

var users = require('../../app/controllers/users.server.controller');

module.exports = function(app) {
  app.route('/users')
    .post(users.create)
    .get(users.list);
};

All you have left to do is run your application by executing the following command:

$ node server

Then, you will be able to retrieve a list of your users by visiting http://localhost:3000/users in your browser.

Advanced querying using find()

In the preceding code example, the find() method accept two arguments, a MongoDB query object and a callback function, but it can accept up to four parameters:

  • Query: This is a MongoDB query object
  • [Fields]: This is an optional string object that represents the document fields to return
  • [Options]: This is an optional options object
  • [Callback]: This is an optional callback function

For instance, to retrieve only the usernames and e-mails of your users, you would modify your call to look like the following lines of code:

User.find({}, 'username email', function(err, users) {
  ...
});

Furthermore, you can also pass an options object when calling the find() method, which will manipulate the query result. For instance, to paginate through the users collection and retrieve only a subset of your users collection, you can use the skip and limit options as follows:

User.find({}, 'username email', {
  skip: 10,
  limit: 10
}, function(err, users) {
  ...
});

This will return a subset of up to 10 user documents while skipping the first 10 documents.

Note

To learn more about query options, it is recommended that you visit Mongoose official documentation at http://mongoosejs.com/docs/api.html.

Reading a single user document using findOne()

Retrieving a single user document is done using the findOne() method, which is very similar to the find() method, but retrieves only the first document of the subset. To start working with a single user document, we'll have to add two new methods. Add the following lines of code at the end of your app/controllers/users.server.controller.js file:

exports.read = function(req, res) {
  res.json(req.user);
};

exports.userByID = function(req, res, next, id) {
  User.findOne({
    _id: id
  }, function(err, user) {
    if (err) {
      return next(err);
    } else {
      req.user = user;
      next();
    }
  });
};

The read() method is simple to understand; it is just responding with a JSON representation of the req.user object, but what is creating the req.user object? Well, the userById() method is the one responsible for populating the req.user object. You will use the userById() method as a middleware to deal with the manipulation of single documents when performing read, delete, and update operations. To do so, you will have to modify your app/routes/users.server.routes.js file to look like the following lines of code:

var users = require('../../app/controllers/users.server.controller');

module.exports = function(app) {
  app.route('/users')
     .post(users.create)
     .get(users.list);

  app.route('/users/:userId')
     .get(users.read);

  app.param('userId', users.userByID);
};

Notice how you added the users.read() method with a request path containing userId. In Express, adding a colon before a substring in a route definition means that this substring will be handled as a request parameter. To handle the population of the req.user object, you use the app.param() method that defines a middleware to be executed before any other middleware that uses that parameter. Here, the users.userById() method will be executed before any other middleware registered with the userId parameter, which in this case is the users.read() middleware. This design pattern is useful when building a RESTful API, where you often add request parameters to the routing string.

To test it out, run your application using the following command:

$ node server

Then, navigate to http://localhost:3000/users in your browser, grab one of your users' _id values, and navigate to http://localhost:3000/users/[id], replacing the [id] part with the user's _id value.

Updating an existing user document

The Mongoose model has several available methods to update an existing document. Among those are the update(), findOneAndUpdate(), and findByIdAndUpdate() methods. Each of the methods serves a different level of abstraction, easing the update operation when possible. In our case, and since we already use the userById() middleware, the easiest way to update an existing document would be to use the findByIdAndUpdate() method. To do so, go back to your app/controllers/users.server.controller.js file, and add a new update() method:

exports.update = function(req, res, next) {
  User.findByIdAndUpdate(req.user.id, req.body, function(err, user) {
    if (err) {
      return next(err);
    } else {
      res.json(user);
    }
  });
};

Notice how you used the user's id field to find and update the correct document. The next thing you should do is wire your new update() method in your users' routing module. Go back to your app/routes/users.server.routes.js file and change it to look like the following code snippet:

var users = require('../../app/controllers/users.server.controller');

module.exports = function(app) {
  app.route('/users')
     .post(users.create)
     .get(users.list);

  app.route('/users/:userId')
     .get(users.read)
     .put(users.update);

  app.param('userId', users.userByID);
};

Notice how you used the route you previously created and just chained the update() method using the route's put() method. To test your update() method, run your application using the following command:

$ node server

Then, use your favorite REST tool to issue a PUT request, or use curl and execute this command, replacing the [id] part with a real document's _id property:

$ curl -X PUT -H "Content-Type: application/json" -d '{"lastName": "Updated"}' localhost:3000/users/[id]

Deleting an existing user document

The Mongoose model has several available methods to remove an existing document. Among those are the remove(), findOneAndRemove(), and findByIdAndRemove() methods. In our case, and since we already use the userById() middleware, the easiest way to remove an existing document would be to simply use the remove() method. To do so, go back to your app/controllers/users.server.controller.js file, and add the following delete() method:

exports.delete = function(req, res, next) {
  req.user.remove(function(err) {
    if (err) {
      return next(err);
    } else {
      res.json(req.user);
    }
  })
};

Notice how you use the user object to remove the correct document. The next thing you should do is use your new delete() method in your users' routing file. Go to your app/routes/users.server.routes.js file and change it to look like the following code snippet:

var users = require('../../app/controllers/users.server.controller');

module.exports = function(app) { 
  app.route('/users')
    .post(users.create)
    .get(users.list);

  app.route('/users/:userId')
    .get(users.read)
    .put(users.update)
    .delete(users.delete);

  app.param('userId', users.userByID);
};

Notice how you used the route you previously created and just chained the delete() method using the route's delete() method. To test your delete method, run your application using the following command:

$ node server

Then, use your favorite REST tool to issue a DELETE request, or use curl and execute the following command, replacing the [id] part with a real document's _id property:

$ curl -X DELETE localhost:3000/users/[id]

This completes the implementation of the four CRUD operations, giving you a brief understanding of the Mongoose model capabilities. However, these methods are just examples of the vast features included with Mongoose. In the next section, you'll learn how to define default values, power your schema fields with modifiers, and validate your data.

Extending your Mongoose schema

Performing data manipulations is great, but to develop complex applications, you will need your ODM module to do more. Luckily, Mongoose supports various other features that help you safely model your documents and keep your data consistent.

Defining default values

Defining default field values is a common feature for data modeling frameworks. You can add this functionality directly in your application's logic layer, but that would be messy and is generally a bad practice. Mongoose offers to define default values at the schema level, helping you organize your code better and guarantee your documents' validity.

Let's say you want to add a created date field to your UserSchema. The created date field should be initialized at creation time and save the time the user document was initially created; a perfect example of when you can utilize a default value. To do so, you'll have to change your UserSchema, so go back to your app/models/user.server.model.js file and change it to look like the following code snippet:

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var UserSchema = new Schema({
  firstName: String,
  lastName: String,
  email: String,
  username: String,
  password: String,
  created: {
    type: Date,
    default: Date.now
  }
});

mongoose.model('User', UserSchema);

Notice how the created field is added and its default value defined. From now on, every new user document will be created with a default creation date that represents the moment the document was created. You should also notice that every user document created prior to this schema change will be assigned a created field representing the moment you queried for it, since these documents don't have the created field initialized.

To test your new changes, run your application using the following command:

$ node server

Then, use your favorite REST tool to issue a POST request or use cURL, and execute the following command:

$ curl -X POST -H "Content-Type: application/json" -d '{"firstName":"First", "lastName":"Last","email":"[email protected]","username":"username","password":"password"}' localhost:3000/users

A new user document will be created with a default created field initialized at the moment of creation.

Using schema modifiers

Sometimes, you may want to perform a manipulation over schema fields before saving them or presenting them to the client. For this purpose, Mongoose uses a feature called modifiers. A modifier can either change the field's value before saving the document or represent it differently at query time.

Predefined modifiers

The simplest modifiers are the predefined ones included with Mongoose. For instance, string-type fields can have a trim modifier to remove whitespaces, an uppercase modifier to uppercase the field value, and so on. To understand how predefined modifiers work, let's make sure the username of your users is clear from a leading and trailing whitespace. To do so, all you have to do is change your app/models/user.server.model.js file to look like the following code snippet:

var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var UserSchema = new Schema({
  firstName: String,
  lastName: String,
  email: String,
  username: {
    type: String,
    trim: true
  },
  password: String,
  created: {
    type: Date,
    default: Date.now
  }
});

mongoose.model('User', UserSchema);

Notice the trim property added to the username field. This will make sure your username data will be kept trimmed.

Custom setter modifiers

Predefined modifiers are great, but you can also define your own custom setter modifiers to handle data manipulation before saving the document. To understand this better, let's add a new website field to your User model. The website field should begin with 'http://' or 'https://', but instead of forcing your customer to add this in the UI, you can simply write a custom modifier that validates the existence of these prefixes and adds them when necessary. To add your custom modifier, you will need to create the new website field with a set property as follows:

var UserSchema = new Schema({
  ...
  website: {
    type: String,
    set: function(url) {
      if (!url) {
        return url;
      } else {
        if (url.indexOf('http://') !== 0   && url.indexOf('https://') !== 0) {
          url = 'http://' + url;
        }

        return url;
        }
    }
  },
  ...
});

Now, every user created will have a properly formed website URL that is modified at creation time. But what if you already have a big collection of user documents? You can of course migrate your existing data, but when dealing with big datasets, it would have a serious performance impact, so you can simply use getter modifiers.

Custom getter modifiers

Getter modifiers are used to modify existing data before outputting the documents to next layer. For instance, in our previous example, a getter modifier would sometimes be better to change already existing user documents by modifying their website field at query time instead of going over your MongoDB collection and updating each document. To do so, all you have to do is change your UserSchema like the following code snippet:

var UserSchema = new Schema({
  ...
  website: {
    type: String,
    get: function(url) {
      if (!url) {
        return url;
      } else {
if (url.indexOf('http://') !== 0   && url.indexOf('https://') !== 0) {
          url = 'http://' + url;
        }

        return url;
     }
    }
  },
  ...
});

UserSchema.set('toJSON', { getters: true });

You simply changed the setter modifier to a getter modifier by changing the set property to get. But the important thing to notice here is how you configured your schema using UserSchema.set(). This will force Mongoose to include getters when converting the MongoDB document to a JSON representation and will allow the output of documents using res.json() to include the getter's behavior. If you didn't include this, you would have your document's JSON representation ignoring the getter modifiers.

Note

Modifiers are powerful and can save you a lot of time, but they should be used with caution to prevent unpredicted application behavior. It is recommended you visit http://mongoosejs.com/docs/api.html for more information.

Adding virtual attributes

Sometimes you may want to have dynamically calculated document properties, which are not really presented in the document. These properties are called virtual attributes and can be used to address several common requirements. For instance, let's say you want to add a new fullName field, which will represent the concatenation of the user's first and last names. To do so, you will have to use the virtual() schema method, so a modified UserSchema would include the following code snippet:

UserSchema.virtual('fullName').get(function() {
  return this.firstName + ' ' + this.lastName;
});

UserSchema.set('toJSON', { getters: true, virtuals: true });

In the preceding code example, you added a virtual attribute named fullName to your UserSchema, added a getter method to that virtual attribute, and then configured your schema to include virtual attributes when converting the MongoDB document to a JSON representation.

But virtual attributes can also have setters to help you save your documents as you prefer instead of just adding more field attributes. In this case, let's say you wanted to break an input's fullName field into your first and last name fields. To do so, a modified virtual declaration would look like the following code snippet:

UserSchema.virtual('fullName').get(function() {
  return this.firstName + ' ' + this.lastName;
}).set(function(fullName) {
  var splitName = fullName.split(' '); 
  this.firstName = splitName[0] || ''; 
  this.lastName = splitName[1] || ''; 
});

Virtual attributes are a great feature of Mongoose, allowing you to modify document representation as they're being moved through your application's layers without getting persisted to MongoDB.

Optimizing queries using indexes

As we previously discussed, MongoDB supports various types of indexes to optimize query execution. Mongoose also supports the indexing functionality and even allows you to define secondary indexes.

The basic example of indexing is the unique index, which validates the uniqueness of a document field across a collection. In our example, it is common to keep usernames unique, so in order to tell that to MongoDB, you will need to modify your UserSchema definition to include the following code snippet:

var UserSchema = new Schema({
  ...
  username: {
    type: String,
    trim: true,
    unique: true
  },
  ...
});

This will tell MongoDB to create a unique index for the username field of the users collections. Mongoose also supports the creation of secondary indexes using the index property. So, if you know that your application will use a lot of queries involving the email field, you could optimize these queries by creating an e-mail secondary index as follows:

var UserSchema = new Schema({
  ...
  email: {
    type: String,
    index: true
  },
  ...
});

Indexing is a wonderful feature of MongoDB, but you should keep in mind that it might cause you some trouble. For example, if you define a unique index on a collection where data is already stored, you might encounter some errors while running your application until you fix the issues with your collection data. Another common issue is Mongoose's automatic creation of indexes when the application starts, a feature that could cause major performance issues when running in a production environment.

Defining custom model methods

Mongoose models are pretty packed with both static and instance predefined methods, some of which you already used before. However, Mongoose also lets you define your own custom methods to empower your models, giving you a modular tool to separate your application logic properly. Let's go over the proper way of defining these methods.

Defining custom static methods

Model static methods give you the liberty to perform model-level operations, such as adding extra find methods. For instance, let's say you want to search users by their username. You could of course define this method in your controller, but that wouldn't be the right place for it. What you're looking for is a static model method. To add a static method, you will need to declare it as a member of your schema's statics property. In our case, adding a findOneByUsername() method would look like the following code snippet:

UserSchema.statics.findOneByUsername = function (username, callback) {
  this.findOne({ username: new RegExp(username, 'i') }, callback);
};

This method is using the model's findOne() method to retrieve a user's document that has a certain username. Using the new findOneByUsername() method would be similar to using a standard static method by calling it directly from the User model as follows:

User.findOneByUsername('username', function(err, user){
  ...
});

You can of course come up with many other static methods; you'll probably need them when developing your application, so don't be afraid to add them.

Defining custom instance methods

Static methods are great, but what if you need methods that perform instance operations? Well, Mongoose offers support for those too, helping you slim down your code base and properly reuse your application code. To add an instance method, you will need to declare it as a member of your schema's methods property. Let's say you want to validate your user's password with an authenticate() method. Adding this method would then be similar to the following code snippet:

UserSchema.methods.authenticate = function(password) {
  return this.password === password;
};

This will allow you to call the authenticate() method from any User model instance as follows:

user.authenticate('password');

As you can see, defining custom model methods is a great way to keep your project properly organized while making reuse of common code. In the upcoming Lessons, you'll discover how both the instance and static methods can be very useful.

Model validation

One of the major issues when dealing with data marshaling is validation. When users input information to your application, you'll often have to validate that information before passing it on to MongoDB. While you can validate your data at the logic layer of your application, it is more useful to do it at the model level. Luckily, Mongoose supports both simple predefined validators and more complex custom validators. Validators are defined at the field level of a document and are executed when the document is being saved. If a validation error occurs, the save operation is aborted and the error is passed to the callback.

Predefined validators

Mongoose supports different types of predefined validators, most of which are type-specific. The basic validation of any application is of course the existence of value. To validate field existence in Mongoose, you'll need to use the required property in the field you want to validate. Let's say you want to verify the existence of a username field before you save the user document. To do so, you'll need to make the following changes to your UserSchema:

var UserSchema = new Schema({
  ...
  username: {
    type: String,
    trim: true,
    unique: true,
    required: true
  },
  ...
});

This will validate the existence of the username field when saving the document, thus preventing the saving of any document that doesn't contain that field.

Besides the required validator, Mongoose also includes type-based predefined validators, such as the enum and match validators for strings. For instance, to validate your email field, you would need to change your UserSchema as follows:

var UserSchema = new Schema({
  ...
  email: {
    type: String,
    index: true,
    match: /.+@.+..+/
  },
  ...
});

The usage of a match validator here will make sure the email field value matches the given regex expression, thus preventing the saving of any document where the e-mail doesn't conform to the right pattern.

Another example is the enum validator, which can help you define a set of strings that are available for that field value. Let's say you add a role field. A possible validation would look like this:

var UserSchema = new Schema({
  ...
  role: {
    type: String,
    enum: ['Admin', 'Owner', 'User']
  },
  ...
});

The preceding condition will allow the insertion of only these three possible strings, and thus prevent you from saving the document.

Note

To learn more about predefined validators, it is recommended you to visit http://mongoosejs.com/docs/validation.html for more information.

Custom validators

Other than predefined validators, Mongoose also enables you to define your own custom validators. Defining a custom validator is done using the validate property. The validate property value should be an array consisting of a validation function and an error message. Let's say you want to validate the length of your user's password. To do so, you would have to make these changes in your UserSchema:

var UserSchema = new Schema({
  ...
  password: {
    type: String,
    validate: [
      function(password) {
        return password.length >= 6;
      },
      'Password should be longer'
    ]
  },
  ...
});

This validator will make sure your user's password is at least six characters long, or else it will prevent the saving of documents and pass the error message you defined to the callback.

Mongoose validation is a powerful feature that allows you to control your model and supply proper error handling, which you can use to help your users understand what went wrong. In the upcoming Lessons, you'll see how you can use Mongoose validators to handle the user's input and prevent common data inconsistencies.

Using Mongoose middleware

Mongoose middleware are functions that can intercept the process of the init, validate, save, and remove instance methods. Middleware are executed at the instance level and have two types: pre middleware and post middleware.

Using pre middleware

Pre middleware gets executed before the operation happens. For instance, a pre-save middleware will get executed before the saving of the document. This functionality makes pre middleware perfect for more complex validations and default values assignment.

A pre middleware is defined using the pre() method of the schema object, so validating your model using a pre middleware will look like the following code snippet:

UserSchema.pre('save', function(next) {
  if (...) {
    next()
  } else {
    next(new Error('An Error Occured'));
  }
});

Using post middleware

A post middleware gets executed after the operation happens. For instance, a post-save middleware will get executed after saving the document. This functionality makes post middleware perfect to log your application logic.

A post middleware is defined using the post() method of the schema object, so logging your model's save() method using a post middleware will look something like the following code snippet:

UserSchema.post('save', function(next) {
  if(this.isNew) {
    console.log('A new user was created.');
  } else {
    console.log('A user updated is details.');
  }
});

Notice how you can use the model isNew property to understand whether a model instance was created or updated.

Mongoose middleware are great for performing various operations, including logging, validation, and performing various data consistency manipulations. But don't worry if you feel overwhelmed right now because later in this book, you'll understand them better.

Note

To learn more about middleware, it is recommended that you visit http://mongoosejs.com/docs/middleware.html.

Using Mongoose DBRef

Although MongoDB doesn't support joins, it does support the reference of a document to another document using a convention named DBRef. Mongoose includes support for DBRefs using the ObjectID schema type and the use of the ref property. Mongoose also supports the population of the parent document with the child document when querying the database.

To understand this better, let's say you create another schema for blog posts called PostSchema. Because a user authors a blog post, PostSchema will contain an author field that will be populated by a User model instance. So, a PostSchema will have to look like the following code snippet:

var PostSchema = new Schema({
  title: {
    type: String,
    required: true
  },
  content: {
    type: String,
    required: true
  },
  author: {
    type: Schema.ObjectId,
    ref: 'User'
  }
});

mongoose.model('Post', PostSchema);

Notice the ref property telling Mongoose that the author field will use the User model to populate the value.

Using this new schema is a simple task. To create a new blog post, you will need to retrieve or create an instance of the User model, create an instance of the Post model, and then assign the post author property with the user instance. An example of this should be as follows:

var user = new User();
user.save();

var post = new Post();
post.author = user;
post.save();

Mongoose will create a DBRef in the MongoDB post document and will later use it to retrieve the referenced document.

Since the DBRef is only an ObjectID reference to a real document, Mongoose will have to populate the post instance with the user instance. To do so, you'll have to tell Mongoose to populate the post object using the populate() method when retrieving the document. For instance, a find() method that populates the author property will look like the following code snippet:

Post.find().populate('author').exec(function(err, posts) {
  ...
});

Mongoose will then retrieve all the documents in the posts collection and populate their author attribute.

DBRefs are an awesome feature of MongoDB. Mongoose's support for this feature enables you to calmly rely on object references to keep your model organized. Later in this book, we'll use DBRef to support our application logic.

Note

To find out more about DBRefs, it is recommended that you visit http://mongoosejs.com/docs/populate.html.

Using Mongoose DBRef
Using Mongoose DBRef
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.239.82