Integrating Devise with Backbone.js

For the impatient: view the code, or see the demo.

Update 30 May 2012: The original version of the application and this article referenced the backbone.modelbinding project by Derick Bailey. Per the readme, that project has been abandoned and I have replaced backbone.modelbinding with the preferred backbone.modelbinder project. All code and referenced samples below have been updated accordingly.

Backbone.js UI implementation for Devise, Ruby on Rails authentication

Several weeks ago I set out to create a single page web app leveraging Backbone.js on the front end with Ruby on Rails powering the back end. In the architecture for this application, Rails is essentially just a JSON API. All of the front-end magic happens with Backbone.js and related friends. Granted, Rails is doing a lot more boilerplate than just acting as an API, but the separation of the UI code and back-end code is a lot clearer in this model.

One of the first things I need on almost all new projects is a way to handle user registration, authentication, and authorization. This project was no different. Fortunately, the Devise Ruby Gem handles this for us in a very clean, configurable way, and to make matters even better, it natively responds to JSON calls. This made integrating it into my single page web app a breeze.

Arguably, doing this is unnecessary as Devise ships with its own UI implementation. However, for the sake of UI consistency and richness, re-implementing it in Backbone is fairly trivial and was a good learning experience. I've extracted this part of the app into a sample rails project which can be found at https://github.com/jhuckabee/backbone_devise. I've also published a working demo at http://backbonedevise.herokuapp.com.

To implement the Devise UI in Backbone.js I started by creating 3 Backbone models, one each for authentication, registration, and password retrieval.

UserRegistration:

  BD.Models.UserRegistration = Backbone.Model.extend({
    url: '/users.json',
    paramRoot: 'user',

    defaults: {
      "email": "",
      "password": "",
      "password_confirmation": ""
    }
  });

UserSession:

BD.Models.UserSession = Backbone.Model.extend({
  url: '/users/sign_in.json',
  paramRoot: 'user',

  defaults: {
    "email": "",
    "password": ""
  }

});

UserPasswordRecovery:

BD.Models.UserPasswordRecovery = Backbone.Model.extend({
  url: '/users/password.json',
  paramRoot: 'user',

  defaults: {
    "email": ""
  }
});

For each of the models I created a corresponding view and used the modelbinding plugin to bind the model attributes to their respective form elements. This left the view with only being responsible for handling form submissions and taking appropriate action based on the response from Rails. In this case I'm just displaying appropriate error messages or updating the layout as a result of a successful login.

Here is the login view:

BD.Views.Unauthenticated = BD.Views.Unauthenticated || {};

BD.Views.Unauthenticated.Login = Backbone.Marionette.ItemView.extend({
  template: 'unauthenticated/login',

  events: {
    'submit form': 'login'
  },

  initialize: function() {
    this.model = new BD.Models.UserSession();
    this.modelBinder = new Backbone.ModelBinder();
  }

  onRender: function() {
    this.modelBinder.bind(this.model, this.el);
  },

  login: function(e) {

    var self = this,
        el = $(this.el);

    e.preventDefault();

    el.find('input.btn-primary').button('loading');
    el.find('.alert-error').remove();

    this.model.save(this.model.attributes, {
      success: function(userSession, response) {
        el.find('input.btn-primary').button('reset');
        BD.currentUser = new BD.Models.User(response);
        BD.vent.trigger("authentication:logged_in");
      },
      error: function(userSession, response) {
        var result = $.parseJSON(response.responseText);
        el.find('form').prepend(BD.Helpers.Notifications.error(result.error));
        el.find('input.btn-primary').button('reset');
      }
    });

  }

});

It's also important to point out that in addition to Backbone.js, I'm also using a few additional libraries to help with application structure and Rails integration. These are:

  • backbone.marionette:
    Bakes in some nice implementation patterns for dealing with large
    javascript applications like the notion of layouts, composite/item views,
    regions, and application-wide pub/sub.
  • backbone.modelbinder:
    Provides a simple way to bind model attributes to HTML elements a la Knockout.js.
  • backbone-rails' sync implementation:
    The backbone-rails gem provides an out-of-the-box solution for integrating
    Rails and Backbone.js. I'm only using their Backbone.sync implementation
    which makes Backbone.js and Rails play nicely together when talking back
    and forth.

So far, I feel like these technologies are playing very nicely together. Rails has made flushing out my own JSON API's extremely easy, and Backbone has made working on the client side a much more clean and enjoyable experience.