Simple Model Relationships with Backbone.js

Update: Backbone.associate provides a tiny, consistent implementation of the relationships described here. In a hurry? Clone it from github. Otherwise, read on.

Backbone brings an awful lot of tasty sugar to common app-development tasks, but the single most compelling argument for adopting it just might be how neatly it wraps up RESTful AJAX requests. Given a collection of models, all that’s needed to retrieve a server-side update is a single call to Collection.fetch():

var post = Backbone.Model.extend({});

var postCollection = Backbone.Collection.extend({
  model: post,
  url: '/posts'
});

posts = new postCollection();
posts.fetch(); // GET /posts

When /posts points to a list of posts exposed by the API, Backbone’s fetch method will return shortly with a corresponding list of models. No surprises there.

Things get a bit tricker when a request goes looking for a subset of the full collection. Instead of asking for all the posts on a site, maybe an application needs to grab only the posts that belong to a particular blog. The quick answer might be to modify the url function to retrieve the blog’s posts:

// Overly coupled model relationship. Don't do this:
postCollection = Backbone.Collection.extend({
    model: post
    url: function () { return '/blog/' + this.blogId + '/posts'; }
});

posts = new postCollection();
posts.fetch(); // GET /blog/3/posts

But what happens when a post can be related to more than one model? It doesn’t make sense to ask for all of the posts authored by a specific user from /blog/14/posts. Rather, it would be much clearer to offer a new resource URL:

/users/me/posts

The trouble is that the URL function currently assigned by postCollection knows nothing about /users. This is the exact problem that relational models set out to solve. Paul Uithol’s backbone-relational library offers a comprehensive implementation of many familiar relationships, but for lightweight projects it might be worth considering how individual relationships may be defined.

Returning to the problem at hand, there are three obvious solutions for describing post relationships. In order of ascending practicality, they are:

  1. Create a convoluted helper for telling the url function where to look
  2. ExtendpostCollection, redefining url for each relationship
  3. Create relational methods in the collection and adjust the url method accordingly

Extending the collection isn’t out of the question in certain situations (e.g., where child models will not belong to more than one parent), but the third choice is the one suggested in the documentation. Put into action, the “has-many” relationship between a blog and its posts turns out like this:

var postCollection = Backbone.Collection.extend({
  model: post,
  url: function () { return '/posts'; }
});

var blog = Backbone.Model.extend({

  defaults: { posts: [] },

  initialize: function () {
    var self = this;
    this.posts = new postCollection(this.get('posts'));
    this.posts.url = function () {
        return self.url() + '/posts';
    };
  },

  urlRoot: '/blog'

});

The magic here takes place when the collection is initialized. First, a new postCollection is drawn from whatever is stored in the model’s posts attribute. That makes it possible to pass in an array of posts directly to the blog—very handy for dealing with responses from a JSON API:

var data = {
  id: 42,
  posts:[
    { id: 13, title: 'hello, world' }
  ]
};

var myBlog = new blog(data);

The second step is to assign the posts’ URL so that requests referencing individual posts will be directed through the blog’s endpoint. Now, each post’s URL will be computed relative to the parent model

myBlog.posts.each(function (post) {
  console.log(post.url()); // /blog/42/posts/13
});​

The possibilities beyond this fairly trivial example aren’t hard to imagine. Mirroring the “has-many” posts relationship for a blog resource as a “belongs-to” relationship for each post would simply be a matter of creating a reverse reference in each post. But any more serious relationships are probably a hint that it’s time to seriously consider adopting a more serious Model library. No need to reinvent the wheel. For simple relationships, however, it only takes moments to adjust Backbone’s built-in Models to suit.

Related Links

Hey, I'm RJ! For more learnings about software and management, find me @rjzaworski or sign up for my semi-regular newsletter.

Let’s keep in touch

Send me timely updates on software, product, and process.