Let’s consider how we can set up proper initialization, security, and normalization for a basic Mongoose Schema for a web application user. We’ll be using a real-world example instead of the silly trivial examples people usually use in these tutorials.

This tutorial is aimed at users who already understand how Mongoose/MongoDB works. Hopefully you understand what required, default, and unique schema attributes do already.

Briefly look over the following example Schema:

var User = mongoose.Schema({
  color: {
    type: String,
    default: '#FFFFFF',
    validate: [isHexColor, 'color is not a Hex Color'],
  },
  enableMessaging: {
    type: Boolean,
    default: false,
  },
  username: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    // there are many tutorials on encryption, so make sure you do it!
    type: String,
    required: true,
  },
});

function isHexColor(s) {
  return /^#(\[A-Fa-f0-9\]{6}|\[A-Fa-f0-9\]{3})$/.test(s);
}

Here we have a basic Mongoose Schema with a unique required username, a required password, a string attribute, and a boolean attribute. Let’s talk about how we can take advantage of Mongoose features to wrangle this data in an efficient manner.

Using Validation Functions

You’ll notice we use a validation function on our color attribute. This checks that whenever we try and save a color it is formatted properly as a Hex color. If it fails our check, mongoose will not save the document and return an error instead. You should use validation functions whenever you are dealing with restrictive/formatted data. Instead of writing the validation code for every api route, you can add it here in the Schema just once. This makes it easier to update, and lowers code repetition.

Protecting Private Attributes and Sensitive Data

When you use express to send a document to the client through res.send(), express will call toJSON to transform the document prior to sending it to the client. You can take advantage of this by setting up each Schema’s toJSON’s transform attribute to remove sensitive data that you don’t want the client to ever see. For example:

User.set('toJSON', {
  transform: function (doc, user, options) {
    delete user.password;
    return user;
  },
});

or you can use a whitelist approach

User.set('toJSON', {
  transform: function (doc, user, options) {
    return {
      name: user.name,
      color: user.color,
    };
  },
});

Again, following the theme of code consolidation, we don’t want to be worried about having to delete sensitive data every time we do a res.send(). Instead we can define it here once, and never worry about it again!

Using Virtual Attributes

Using virtual attributes is a great way to help normalize and lower the amount of data you store in your database. For example, if we wanted to easily be able to access each users profile url, we could add a virtual attribute like:

User.virtual('url').get(function () {
  return '/' + this.username + '/profile';
});

Tip: You can include virtuals when sending your document to the client by using { virtuals:true } attribute within the set('toJSON') object:

Schema.set('toJSON', {
  transform: ...,
  virtuals: true
})

This way our client will have access to the url attribute as well!

A Note on Querying Default Values

Mongoose sends your queries directly to MongoDB, therefore it won’t know about default values that haven’t been saved to the db yet. This case only arises when you’ve added a new default attribute to a Schema after docs of that type have already been saved to the db. However, this tends to happen a lot in growing/changing applications and can save you headaches later on down the road.

In our example if we added the color feature after hundreds of our users had already signed up, and tried query our database for users with { color: '#FFFFFF' }, then we might not get any of the original users unless their docs have been re-saved since we added that default value.

So to combat this you can either forcibly add the default value to all users when you add a new attribute, or you can check for non-existent values as well. For example:

$or: [{ color: { $exists: false } }, { color: '#FFFFFF' }];