LJ Archive

At the Forge

Backbone.js and Rails

Reuven M. Lerner

Issue #207, July 2011

How to combine two MVC frameworks—one on the client, one on the server—for even more powerful applications.

Last month, I started looking at Backbone.js, an increasingly popular framework for Web applications written in JavaScript. If you think that Web development frameworks exist only on the server, you're in for a surprise. A growing number of frameworks have sprung up on the browser, written in JavaScript and meant to execute completely within that environment. You can think of such frameworks as having less to do with the Web and more to do with the creation of a full-fledged application inside a browser, using a combination of HTML, CSS and (of course) JavaScript.

Many client-side frameworks use the established MVC (model-view-controller) paradigm that has existed for several decades and has proven to be both useful and powerful in developing both Web and desktop applications. By keeping the code organized in a standard way, the design and maintenance of the application are simplified to a great degree, allowing you to concentrate on your application's domain, rather than more general architectural decisions.

In MVC, one set of objects (the models) represents the data, sometimes known as the “business objects”, and all of their associated logic. The “view” consists of what the user will see on the screen, either initially or after some manipulations have been performed. Finally, the “controller” objects receive requests from the user and route them to the appropriate models and views. Each implementation of MVC is slightly different, but the separation of authority into these three categories helps a great deal.

Last month, I built a very simple appointment calendar, allowing you to indicate with whom you are meeting, and when (which has been broken into two files, Listings 1 and 2). Aside from the numerous usability problems that were associated with that application (not surprising for a magazine tutorial), there was one clear issue with this appointment calendar. The moment you close your browser window, the entire calendar disappears from memory, never to return.

For me, and I assume for many other Web developers, this is a new situation. For years, my Web applications largely have been a visual representation of what I had stored in the database. True, users always could change things without clicking a “submit” button, but AJAX reduced that risk. And, even if you lost one page's worth of data, it usually was the minority of what someone had entered. In the world of JavaScript applications, by contrast, you don't lose only a little bit, you lose the entire thing! This is clearly unacceptable.

For this reason, browser-based MVC applications increasingly are hooking together with server-side MVC applications. The JavaScript model in such an application saves itself not to disk or to a database, but via a RESTful URL on a server somewhere. That server is, in turn, running its own MVC application, and accepts the data, stores it and makes it available to other applications. I've started to call these applications “MVC-squared”, because they involve two separate MVC applications—although clearly, neither is all that useful without the other. Different client-side frameworks tackle this in different ways, and my hope is to cover a few other JavaScript frameworks in the coming months, so you can compare the different techniques they use to accomplish similar goals.

Setting Up Your Server

To begin, you need to set up a Web application on your server. I use Ruby on Rails here, but so long as you're using a server application that understands and adheres to the REST conventions, you should be just fine. First, I create a Rails application called “appointments” that uses PostgreSQL as its back-end database:

$ rails new appointments -d postgresql

I create a PostgreSQL database for the development environment:

$ createuser -P appointments

Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) y
Shall the new role be allowed to create more new roles? (y/n) n

$ createdb -U appointments appointments_appointments_development

Given that I gave the “appointments” user a password (also “appointments”), I have go into config/database.yml and add a value in the “password” field for the “development” environment. I always like to test that I can connect to the database using the rails db -p command, which connects me to the “psql” client program using the password in the development environment. If this works, I know my database connection is working correctly.

Now, let's create a resource in the Rails application—an appointment. The most important thing here is to ensure that your data is aligned between the server and the client, in both name and type. In this example application, I'm storing each appointment as three fields: the name of the person with whom I'm meeting (“person”), the date and time of the meeting (“starts_at”) and notes about the meeting (“notes”). The Backbone.js application uses these same names, but assumes that all are text strings; I define the starts_at column as being of type “timestamp”.

Now, let's create the resource, along with a skeleton controller and views:

rails g scaffold appointment person:text starts_at:timestamp note:text

I should note that this is a highly non-normalized database definition, which means that it'll be highly inefficient, as well as inflexible. I'm a big fan of normalization, but right now, that's less important than seeing how the parts fit together.

The first and most important thing that this generator does is create a migration (in db/migrations). The migration looks like this:


class CreateAppointments < ActiveRecord::Migration
  def self.up
create_table :appointments do |t|
  t.text :person
  t.timestamp :starts_at
  t.text :note

  t.timestamps
end
  end

  def self.down
drop_table :appointments
  end
end

I always like to tighten up the definitions a bit, as well as provide some indexes that'll allow me to search through the database more efficiently:


class CreateAppointments < ActiveRecord::Migration
  def self.up
create_table :appointments do |t|
  t.text :person, :null => false
  t.timestamp :starts_at, :null => false
  t.text :note, :null => true

  t.timestamps
end

add_index :appointments, :person
add_index :appointments, :starts_at
add_index :appointments, :note
  end

  def self.down
drop_table :appointments
  end
end

Run the migration with rake db:migrate. Assuming that all went well, the database now contains an “appointments” table, which you can access via ActiveRecord.

The generator did a bit more than just create the migration, a basic controller and views; it also added the line:

resource :appointments

to the config/routes.rb file. This one line has a great deal more influence than you might think. It tells Rails that you want to expose the “appointments” object as a resource, using the seven standard controller actions (index, show, new, create, edit, update and delete) that correspond to HTTP request methods. By ensuring that your object is exposed using REST, you more easily can hook it up to your Backbone.js application.

You also will need to adjust your controller (Listing 3). Backbone.js communicates with the server by default using JSON, which means that in each scaffold controller action, you either can add a new line for the JSON format or replace it entirely. For example, after adding JSON to the “index” action, it looks like this:

def index
  @appointments = Appointment.all

  logger.warn "Found '#{@appointments.size}' appointments"

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @appointments }
    format.json  { render :json => @appointments }
  end
end

Removing non-JSON entirely lets you rewrite it as:

def index
  Appointment.all.to_json
end

which is both shorter and more understandable.

Now, this example Rails application might be small, but it already works. You can start the server with rails s and visit it at port 3000 (the default). Because I didn't remove the original index page from the “public” directory, simply going to localhost:3000 is going to show just the default “Welcome to Rails” page. But, if you go to http://localhost:3000/appointments/, you see that the scaffolding is available, showing a list of the current appointments (that is, nothing), as well as a link to create a new appointment.

MVC, Please Meet MVC

You now have two distinct applications that share an underlying data model. On the browser is the Backbone.js application, whose “Appointment” model and associated collections allow you to create appointments, as well as view them. And there's the Rails application, which also has an “Appointment” model, as well as ways to view them. Now, let's connect them, such that the Backbone.js model will save and receive its data from the Rails application.

You can get this all to work by adding a “url” method to your model, such that it knows the URLs it can use to retrieve and store data. Let's also add a controller, which will make it easier to organize the code, as well as accomplish various tasks with it.

It turns out that although Backbone.js is an MVC framework, it grew iteratively and over time. Controllers weren't originally part of the framework, which means that as you saw in last month's code example, you even can get away without a controller at all, using only views and models. That alone is quite different from Rails MVC, which wouldn't do much without a controller.

Another difference between Backbone.js controllers and their Rails counterparts is that in Backbone.js, routes—the mappings between URLs and controller actions—are defined right inside the controller, alongside variables and functions. (By contrast, Rails puts such information in a separate configuration file, config/routes.rb.) I also should note that talking about “routes” in the context of a JavaScript application is a bit strange, because the whole point of such an application is that you stay within the same URL. Thus, routes in Backbone.js refer to the portion of the URL following the hash (#) character, originally intended to be used as a named anchor, but today used more by JavaScript.

This means that a route of "" (that is, the empty string) will be run by default when your application is opened. A route of abc will be run when the URL ends with #abc (entered manually in the URL, or from within a link), and a route of abc/:def will be run when the URL ends with #abc/SOMETHING, where SOMETHING can be anything at all and is passed as a parameter to the function invoked by the controller.

Controllers, like models, collections and views, are objects defined by Backbone.js, which you extend in order to use. A simple controller might look like this:

AppController = Backbone.Controller.extend({
routes: {
    "" : "index",
    "say/:something" : "say"
},

index: function() {
  alert("Now invoked controller index");
},

say: function(something) {
  alert("You said " + something + "!");
}

});

This controller has two defined routes. If the hash character isn't present, it will invoke the “index” function. If the hash character is there, and if there is the word “say”, a parameter and then some text following it, it will invoke the “say” function, printing the contents of the parameter.

Notice that the routes here are extremely flexible. You aren't restricted to the standard Rails-style RESTful routes. But of course, you aren't dealing with REST anymore, because this is the application itself, not a resource for other applications to use. If you insert the following HTML into your document:


<p>Internal link <a href="#say/something_at_all">here</a></p>

this adds a link that will result in the invocation of say("something_at_all").

If you put the above into your Backbone.js application and reload, you'll soon see that...well, nothing happens. That's because you might have defined the controller object, but you haven't created it. Thus, you need to put the following line into your code to create a new AppController object:

var myController = new AppController;

Now if you try to reload, you'll find that...well, once again, things won't work. That's because controllers in Backbone.js are integrated with a history object (Backbone.History), which keeps track of the URLs you recently visited. If your browser supports the “onhashchange” event, Backbone.js will take advantage of it. But if it doesn't, Backbone.js will poll the browser 20 times each second, checking to see if the URL has changed, and if so, firing the appropriate function, based on the current URL.

So, you need to start the Backbone.History object:

Backbone.history.start();

With all of these in place—routes inside the controller object definition, an actual controller object and a running history object—Backbone.js springs into action. If you simply go to the URL for your page, the "" (empty string) route will execute the “index” function.

In a real-world application, you don't want the index function to put up an alert box. Rather, you'll almost certainly want it to load any data that already might have been saved on the server. In other words, after you have initialized all of your objects, you'll want to create the controller, which then will load objects from the remote server.

Saving and Loading

If you haven't quite grasped what we're doing here, consider this. In a typical server-side MVC application, the view is shown to the user, and the model is grabbed from a database. That hasn't changed here, except that the “user” for this Rails application is the Backbone.js application. The MVC of the Backbone.js application, by contrast, expects that its model will come from the server's view output.

How does this Backbone.js program know how to load information from the server? As mentioned above, you can add a “url” function to both your model and to your collection. For example, the start of your collection now can look like this:

Appointments = Backbone.Collection.extend({
   url: "/appointments.json",

The .json suffix tells Rails that communication will be done in the JSON format. When you receive output, it will be in JSON. Backbone.js can handle XML output as well, but all of the cool kids use JSON nowadays, so let's do that as well.

Once you tell the Appointments collection its URL, you then can ask it to retrieve data from that URL and populate itself with Appointment models. You simply can invoke the built-in fetch() method, which executes asynchronously, but even better, you can pass the fetch() method an object with “success” and “error” attributes, each of which contains a function that is executed based on the result from invoking fetch().

You invoke fetch(), as you might expect, from the controller, on the “index” route, which happens when the page is loaded without any hash tags. In other words, your initial visit to the page will invoke the controller's “index” action, which will tell the Appointments collection to load data from the server via the URL /appointments.json.

If all you wanted to do was retrieve the list of appointments, that would be fine. But you also want to display that list to the user. This means you need to iterate over each received appointment object and display it for the user. Fortunately, Backbone.js is built on top of the Underscore library for JavaScript, giving you access to all sorts of useful functions, including many for iteration. So when you retrieve the data, you then can set the following to be your “success” function, iterating over each item that you received from the server, and using a view method to add a new row to your HTML table:

success: function(response) {
    appview.appointments.each(function(item) {
                  appview.add_appointment_row(item);
              }
                 );
    $("#number-of-appointments").html(appview.appointments.length);
}

Finally, you also will need to save any new appointment that you have created, using the HTML form, to the server. In order to do this, you must set the “url” attribute on the model (as opposed to the collection). This is a bit more complicated than the URL for the collection, because it must include the unique ID of the appointment you are saving, but only if you're updating an existing model. (Which, I should note, is impossible in the current version of the software, but it would be a welcome feature in future versions.) If you're creating a new appointment, you need not send any ID at all:

url : function() {
var base = '/appointments/';
if (this.isNew()) return base;
return base + this.id + '.json';
}

Now, when does a model get saved? Immediately after creating it, at least in this code. In the view function add_appointment, which executes (based on a jQuery callback) when someone clicks on the “add appointment” button, you first create a new appointment:

var new_appointment =
    new Appointment({person: person, starts_at: starts_at, 
                    ↪note: note});

You then add this new appointment to your collection:

this.appointments.add(new_appointment);

Finally, you also save the appointment to the server:

new_appointment.save();

Because your Appointment object is based on the Backbone.js model, it automatically knows how to save itself to the URL. And sure enough, when you add a new appointment, an AJAX query automatically fires in the background, sending a POST or PUT HTTP request to the server (as appropriate).

Conclusion

It can take a bit of time to get used to this MVC-squared (or whatever you wish to call it) paradigm. Once you get used to the fact that your server is being used for storage, data validation and providing a good data representation, but that the user interactions are taking place in the browser and in a separate application, things start to fall into place. Backbone.js is a great way to get started with this sort of application, especially if you're familiar with jQuery and want to use jQuery widgets in your application.

Reuven M. Lerner is a longtime Web developer, architect and trainer. He is a PhD candidate in learning sciences at Northwestern University, researching the design and analysis of collaborative on-line communities. Reuven lives with his wife and three children in Modi'in, Israel.

LJ Archive