Single Page Web Apps with Backbone.js

     

About a month ago, we decided that we needed to give the front-end of sendhub.com a face-lift. The site looked a bit dated, and it just didn’t feel as snappy as we wanted it to. All pages were rendered almost entirely on the server, and only a couple of places used Ajax to pull in data asynchronously. In parallel to this, we were also in the process of releasing our developer API which we knew badly needed hardening before we would be willing to open it up to the developer community.

Seeing an opportunity here, we decided to re-build the front end of our site using a single-page application model. The goal was to leverage the API as much as possible in the process to iron out the finer details. As it turns out, not only did this serve us well in our efforts to create what we think is a pretty good API V1, it also changed the way we go about creating great web applications for our users.

What follows is an explanation of the things we learned along the way. Hopefully it will help you to do the same.

REST + Backbone = Gravy

We built our app using Backbone.js. Backbone.js was built to play well with REST. The more that you follow proven REST design patterns in your API design, the less you have to do to get your backbone collections talking to your server. Specifically, you should aim to follow the common pattern of designing URI’s around nouns rather than verbs. Furthermore, try to leverage HTTP methods GET, POST, PUT, DELETE etc to perform their respective CRUD actions on those nouns. This ends up simplifying things quite a bit in model and collection objects. As an example, here is the entire collection object for SendHub messages:

SH.collections.MessagesCollection = Backbone.Collection.extend({ 
 model: SH.models.Message, 
 url: SH.config.apiPrefix + "messages/" 
}); 

When we wanted to write the code to send a new message out, all we had to do was create the object to be sent to the server, add it to the collection, and call save() that object.

this.model = new SH.models.Message(); 
this.model.on("sync", this.messageSent, this); 
this.model.on("error", this.showErrorMessages, this); 
var collection = new SH.collections.MessagesCollection(); 
collection.add([this.model]); 
var msgData = {}; //... populate msgdata here... 
this.model.save({...data goes here...}); 

That’s it! Backbone handles all of the request magic for us on save(). All we had to do was provide the url for the collection. Without an API that was designed around common REST patterns, there would be a lot more code here to make Backbone fit into the request/response life-cycle. Yes, for brevity, we did skip the code for the Message model. And to prove that we are not just collecting underpants and ending up with profit, here is the list of functions and the declaration for the Messages model…

SH.models.Message = Backbone.RelationalModel.extend({ 
 validate: function(){ ... }, 
 isScheduled: function(){ ... }, 
 wasSentSameDayAsMessage: function(){ ... } 
}); 

You’ll notice that these functions have nothing to do with saving to or reading things from the server, but rather, encapsulate business logic. This is also a good idea. Be sure to push as much business logic into your models as you can. You’ll find yourself with a much more testable and re-usable code-base in the end. The trick is understanding the difference between view logic, and business logic.

What we like about backbone is that we less often have to deal with nesting or chaining asynchronous callbacks in our code. Nesting callbacks, while sometimes necessary, can be dangerous. It will quickly turn your code-base into a mess of spaghetti code if abused. Instead, we merely listen for sync and error events on our models to respond to asynchronous events in our view code. No callbacks needed!

Client Side Template Rendering

One thing that was important for us at the beginning of our journey towards a single-page app was understanding the tools available for rendering things on the client side. We looked at Mustache, Handlebars and a couple of other rendering libraries. However, at the end of the day, we ended up just using Underscore’s template compiler. Because of Backbone’s dependency on underscore, it meant that there was one less library to load in the page. Even still, we didn’t find ourselves needing all of the features that the other template libraries provided. In fact, we would argue that a lot of logic in the template itself can be a bad thing. Its hard to test logic when its mixed into a template, and even harder to debug when things go wrong. For example, here is our generic template rendering function…

Backbone.View.prototype.renderTemplate = function() { 
 var getTemplate = function(id) { 
   return _.template($("#"+id).html()); 
 }; 
 var compiledTemplates = {};
 var fn = function(id, context) { 
   if(!_(compiledTemplates).has(id)){ 
 compiledTemplates[id] = getTemplate(id);  
   } 
 return compiledTemplates[id](context); 
 };
 return fn; 
}(); 

…where templates would be referenced by ID, using script tags to contain them….

About The Author