Information Expert Principle Applied To Mongoose Models

Adding a method to a Mongoose Model

In programming the Information expert, or the expert principle, is an approach used to determine where to delegate responsibilities. In other words, where should you place the code that completes specific tasks. The Information expert principle will help a developer to place the responsibility in the class with the most information required to fulfill it. In this tutorial we are going to clean up the process of generating JSON Web Tokens to make our code more clear and easier to maintain.


Removing Duplicate Code

If you’ve been following along with our user registration tutorial for Node.js, you know that we are generating a JWT in more than one place currently. Both users.js and auth.js are doing a task using cookie cutter code. We should extract that logic so that there is only one place that generates the token. That way, if anything changes in the future, you make your changes in one place, not many. In our case, we can add a method to a Mongoose model to do this.


Adding A Method To A Mongoose Model

Right now, the User model looks like so.


/models/user.js

const Joi = require('joi');
const mongoose = require('mongoose');

const User = mongoose.model('User', new mongoose.Schema({
    name: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 50
    },
    email: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 255,
        unique: true
    },
    password: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 1024
    }
}));

function validateUser(user) {
    const schema = {
        name: Joi.string().min(5).max(50).required(),
        email: Joi.string().min(5).max(255).required().email(),
        password: Joi.string().min(5).max(255).required()
    };
    return Joi.validate(user, schema);
}

exports.User = User;
exports.validate = validateUser;

In the code above, the second argument to the mongoose.model() function is a new instance of mongoose.Schema(). The first thing we need to do is to extract this to it’s own constant like so.

const userSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 50
    },
    email: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 255,
        unique: true
    },
    password: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 1024
    }
});

With that in place, setting up the user is now a simple matter of this line.

const User = mongoose.model('User', userSchema);

Adding the method to the mongoose model

We can add the method now to the userSchema just like this.

userSchema.methods.generateAuthToken = function () {
    const token = jwt.sign({_id: this._id}, config.get('PrivateKey'));
    return token;
};

The user model now needs to work with both the config and jsonwebtoken packages, so we make sure to include those at the top of the file. The rest of the code is pretty self-explanatory with regard to extracting the user schema, adding a new method, and then creating a new user model.

const config = require('config');
const jwt = require('jsonwebtoken');
const Joi = require('joi');
const mongoose = require('mongoose');

// Extract Schema to it's own constant
const userSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 50
    },
    email: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 255,
        unique: true
    },
    password: {
        type: String,
        required: true,
        minlength: 5,
        maxlength: 1024
    },
});

// Information Expert Principle (add method to model)
userSchema.methods.generateAuthToken = function () {
    const token = jwt.sign({ _id: this._id }, config.get('PrivateKey'));
    return token;
};

// Create new user model
const User = mongoose.model('User', userSchema);

function validateUser(user) {
    const schema = {
        name: Joi.string().min(5).max(50).required(),
        email: Joi.string().min(5).max(255).required().email(),
        password: Joi.string().min(5).max(255).required()
    };
    return Joi.validate(user, schema);
}

exports.User = User;
exports.validate = validateUser;

Now we have a nice method we can use in other places in our code. So now note the changes in auth.js and users.js. We have essentially removed the commented out code and replaced it with a call to user.generateAuthToken().


/routes/auth.js

const Joi = require('joi');
const bcrypt = require('bcrypt');
const _ = require('lodash');
const { User } = require('../models/user');
const express = require('express');
const router = express.Router();

router.post('/', async (req, res) => {
    // First Validate The HTTP Request
    const { error } = validate(req.body);
    if (error) {
        return res.status(400).send(error.details[0].message);
    }

    //  Now find the user by their email address
    let user = await User.findOne({ email: req.body.email });
    if (!user) {
        return res.status(400).send('Incorrect email or password.');
    }

    // Then validate the Credentials in MongoDB match
    // those provided in the request
    const validPassword = await bcrypt.compare(req.body.password, user.password);
    if (!validPassword) {
        return res.status(400).send('Incorrect email or password.');
    }
    // const token = jwt.sign({ _id: user._id }, config.get('PrivateKey'));
    const token = user.generateAuthToken();
    res.send(token);
});

function validate(req) {
    const schema = {
        email: Joi.string().min(5).max(255).required().email(),
        password: Joi.string().min(5).max(255).required()
    };

    return Joi.validate(req, schema);
}

module.exports = router;

/routes/users.js

const bcrypt = require('bcrypt');
const _ = require('lodash');
const { User, validate } = require('../models/user');
const express = require('express');
const router = express.Router();

router.post('/', async (req, res) => {
    // First Validate The Request
    const { error } = validate(req.body);
    if (error) {
        return res.status(400).send(error.details[0].message);
    }

    // Check if this user already exisits
    let user = await User.findOne({ email: req.body.email });
    if (user) {
        return res.status(400).send('That user already exisits!');
    } else {
        // Insert the new user if they do not exist yet
        user = new User(_.pick(req.body, ['name', 'email', 'password']));
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
        await user.save();
        // const token = jwt.sign({ _id: user._id }, config.get('PrivateKey'));
        const token = user.generateAuthToken();
        res.header('x-auth-token', token).send(_.pick(user, ['_id', 'name', 'email']));
    }
});

module.exports = router;

Fantastic!


Testing The Refactor With Postman

That was a nice refactor, but we need to make sure the API still works as intended. Here we test with Postman by sending a new Post request to http://localhost:4000/api/users/ with a JSON object in the body for a new user. Note we get back a proper user object in the response, so this means it worked!
mongoose model custom method working great


Adding a method to a Mongoose Model Summary

In this tutorial we had a quick look at how to add a method to a Mongoose model in order to reduce duplicate code in other areas of our application. You can add as many methods as needed as long as it makes sense and follows the Information Expert Principle.