At GoSquared, we recently migrated our internal admin and user account systems from PHP to Node.js. It brought up some interesting challenges, very different to those that we’re usually faced with, like optimising Node.js for heavy workloads.
Due to its asynchronous nature, user management and admin applications aren’t usually written in Node.js – but it is possible to build a stable, manageable and flexible system.
Classes
Using classes is the nicest way to provide an accessible API for your other apps. Everybody will have a User
class, and all classes should inherit from a Base
class which provides common methods, such as setAttribute
, getAttribute
, insert
, delete
– whatever your system requires.
var util = require('util');
// assume the Base class has common methods already written
var Base = require('./Base');
var User = module.exports = function(id) {
var self = this;
self.name = 'User';
Base.apply(this, arguments);
self.table = 'users';
};
util.inherits(User, Base);
User.prototype.create = function(details, cb) {
var self = this;
self.insert(details, function(err) {
if (err) return cb(err);
// other user creation methods, such as subscribing
// to a mailing list etc. etc.
cb(null);
});
};
Note: with ES6 you can use the new classes if you prefer
Async
To avoid getting into callback hell, async is an essential node module. Although async can help avoid issues, some deep callback nesting is unavoidable and should be accepted – so long as larger functions are split into smaller methods. Using smaller methods and then connecting them together with async is easy and can lead to very clean and readable code. A great benefit of writing asynchronous code is that tasks can be done in parallel, making functions significantly faster in places – we were able to get a few over 20x faster after the migration.
Assuming the methods getAttributes
and getSubscription
already exist…
The Wrong Way
User.prototype.getDetails = function(cb) {
var self = this;
self.getAttributes(['name', 'email'], function(err, attributes) {
if (err) return cb(err);
self.getSubscription(function(err, subscription) {
if (err) return cb(err);
attributes.subscription = subscription;
cb(null, attributes);
});
});
};
The Right Way (cleaner and faster)
User.prototype.getDetails = function(cb) {
var self = this;
async.parallel([
self.getAttributes.bind(self, ['name', 'email']),
self.getSubscription.bind(self)
], function(err, res) {
if (err) return cb(err);
var attributes = res[0];
attributes.subscription = res[1];
cb(null, attributes);
});;
};
Cron Jobs
Most, if not all, admin systems will require tasks to be run on a regular schedule. We use node-cron to avoid having to maintain crontabs.
As these systems are designed to be distributed across any number of servers, we use locking (via redis) to ensure the cronjobs are only run once. Larger jobs that, for example, loop over every user, are split into individual QP jobs and processed evenly by all servers.
Usage by other applications
All production applications at GoSquared are written in Node.js, which is great for maintainability. In the past, all admin functions were called via a secure HTTP API which had additional overhead and wasn’t terribly flexible – so much so that in places raw MySQL queries were being used rather than the API.
The solution is to use the admin system as a node module, which exports the classes and and utilities/handlers. This can then be required by any applications and the methods can be called directly – making everything quicker and significantly more versatile. Create an index.js
(or equivalent) file that can export what’s needed…
var fs = require('fs');
var Admin = module.exports = {};
var classes = fs.readdirSync(__dirname + '/src/classes').sort();
classes.forEach(function(c) {
if (c.substr(-3) !== '.js') return;
// take off JS
var name = c.slice(0,-3);
// don't export the Base class
if (name === 'Base') return;
// define as a getter to improve performance
Object.defineProperty(Admin, name, {
get: function() {
return require(__dirname + '/src/classes/' + name + '.js');
},
enumerable: true
});
});
Then, in your applications, it will need to be added as a dependency (use git tags for versioning) and can be used like so…
// pretend we're serving an HTTP request to get a user's details
var admin = require('admin');
var User = admin.User;
// app is already defined elsewhere (express)
app.get('/user/:id/details', function(req, res) {
var user = new User(req.param('id'));
user.getDetails(function(err, attributes) {
res.json(attributes);
});
});
Conclusions
Although building an admin system in Node.js may not be the obvious choice, it comes with many advantages and has sped up development here at GoSquared. Our applications run faster and are much more flexible, allowing us to optimise them and ultimately provide a better user experience.
Questions? Comments? Let us know via the usual channels – Twitter (@GoSquared), Facebook or email.
Want to work with us and make our user account systems even better? We’re hiring a full-stack engineer – get in touch.