Node.js Boilerplate: Express, Mongo, Cross-Origin, and More…

One of the best things I’ve learned over the past year has been how to quickly stand up some web service APIs using node.js. In that time, I’ve used it for 3 projects:

  • A yammer-like social community for BlueFletch where people can post and comment
  • A backend that simulates an online retail store for a conference demo
  • A side-project to email out instagram pictures on a daily basis (mainly for family that doesn’t have instagram)

I put together a node-js boilerplate that is generic enough to be reused for other applications.  The rest of this post will talk through some of the boilerplate features.

Express

Express is a great nodejs web app framework that seems to be used in the vast majority of node.js projects (purely anecdotal based on my internet searches).  Whereas node.js is lower-lovel, express sits on top of node.js and provides easy ways to manage sessions, manage cookies, and implement a router among other things.

If you’re just getting started with node.js, express is definitely something you’ll want to look into first.

MongoDB and Mongoose

One of the great things about working on small side-projects is learning new technologies.  In the past few years “noSQL” databases have become pretty popular.  Despite all of the negative publicity around MongoDB’s scaling, I thought it would be a great choice for a small app that won’t see much traffic.

I’ve found that the best way to use MongoDB with node.js is to use mongoose.  Mongoose allows you to create schemas so that your application has a little more structure than the complete freedom that MongoDB gives.

For example, this is how the user schema is defined in the boilerplate’s user.model:

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

As you can see, any time a user is created the username and password are required.  Mongoose will not allow you to create a user if one of these two fields isn’t set, even though the MongoDB database would surely allow it.  Furthermore, each field is type casted so that if you try to put a string into the createdDate field mongoose will throw an error.

Session Management

Originally I had my session management configuration looking like this:

self.app.use(express.session({ secret: '!!!my secret goes here!!!' })),

Upon login, I set the req.session.auth = true if the user was successfully authenticated. But the problem was every time the server was restarted it lost all of the sessions. To fix this issue, I built my own module to manage sessions using a rotating cookie: each time a user logged in, the cookie was changed and saved off to the database for the next time a user logs in. The next time the user accesses the page, it checks the cookie against the database and if they match a new session is created and the cookie key is rotated again.

However, I later found a much better solution that uses connect-mongo to store the session in the database.  This means that the sessions don’t go away when the server is restarted.

self.app.use(express.session({
    store: new mongoStore({
        url: self.app.config.mongoConn
    }),
    secret: '!!!my secret goes here!!!',
    cookie: {
        maxAge: self.app.config.sessionExpiration
    }
}));

To implement this when a user logs in, set the req.session.auth = true and req.session.user = [the user object] (in the login.js controller’s login function).  Now the req.session holds the user object and can be used throughout the application.

Password Encryption

I know there are libraries out there, such as passport, to handle authentication, but I found it more interesting to roll my own using bcrypt.

The boilerplate has an example implementation of a user model that works with mongoose.  The pre function for a schema gets executed before a specified event, in this case the save event. This piece of code will check to see if a password was changed as part of the save, and if it has then it will hash the password before saving the user in the database.

UserSchema.pre('save', function (next) {
    var user = this;

    // only hash the password if it has been modified (or is new)
    if (!user.isModified('password')) {
        logger.debug('Password not modified so next()', 'UserSchema.pre', user.username);
        next();
        return;
    }

    // generate a salt
    logger.debug('Generate salt with salt work factor = ' + UserSchema.constants.SALT_WORK_FACTOR, 'UserSchema.pre', user.username);
    bcrypt.genSalt(UserSchema.constants.SALT_WORK_FACTOR, function (err, salt) {
        if (err) {
            logger.error('Error generating salt: ' + err, 'UserSchema.pre', user.username);
            return next(err);
        }

        // hash the password along with our new salt
        logger.debug('Hash the password using the salt', 'UserSchema.pre', user.username);
        bcrypt.hash(user.password, salt, function (err, hash) {
            if (err) {
                logger.error('Error hashing the password using the salt', 'UserSchema.pre', user.username);
                return next(err);
            }
            else {
                logger.debug('Set password to hashed value', 'UserSchema.pre', user.username);
                // override the cleartext password with the hashed one
                user.password = hash;
                next();
            }
        });
    });
});

Implementing the password check during the login authentication process is fairly straightforward:

UserSchema.methods.authenticate = function (plainText, callback) {
    var user = this;
    logger.debug('Entering authenticate()', 'UserSchema.authenticate', user.username);
    bcrypt.compare(plainText, user.password, function (err, isPasswordMatch) {
        // isPasswordMatch returns true if matches, false if doesn't match
        if (err) {
            logger.error('Error from bcrypt.compare', 'UserSchema.authenticate', user.username);
            callback(false);
        }
        else {
            logger.debug('Successful bcrypt.compare; isPasswordMatch = ' + isPasswordMatch, 'UserSchema.authenticate', user.username);
            callback(isPasswordMatch);
        }
    });
}

Cross Origin

Cross-origin requests can be enabled or disabled by uncommenting the allowCrossDomain line in server.js, which will add some headers to the response:

module.exports = function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Origin');
    res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
    next();
}

In the server router there is also a route to send a 200 SUCCESS response to all options:

app.options('/*', function (req, res) {
    res.send(200);
});

Front-End

I won’t go too in depth about the front-end, but it’s setup for a single-page site that uses requirejs and backbonejs.  I really like using backbonejs and the structure it provides, and my team is currently using it on a large-scale enterprise project that involves 20+ developers coding on multiple applications with a shared framework and business components.  I’ve also had success with it on the smaller projects I’ve worked on to better organize my code and avoid spaghetti code.

Wrapping It Up

This post only scratches the surface of what my boilerplate has.  Hopefully other people can find it (or parts of it) useful too.

Posted in My Projects Tagged with: , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *

*