Thursday, August 8, 2013

Managing URL End-Points in Backbone Models and Collections

I had some trouble trying to get my end-points to work properly in Backbone lately. It was further complicated by the fact that what worked in one version stopped working in a newer version. Both the model and collection have a URL property. In theory, you should be able to use different end-points as necessary, however, depending on where you specify it, the behavior is different and, at times, undesirable. I've found that the source for my collection and the source for model are not always the same. In many instances, the collection will use a search API which provides a smaller fieldset but with many filtering options while the model with use an API that includes all the fields and has your normal REST operations. In that situation, you will want to define similar, but not the same URL for the source of the collection and model.

As a specific case, I was using the Rotten Tomatoes API to pull various lists of movies. Each list has a subset of all the available fields defined for a specific movie. To get the full detail, you need to use a different API end-point:

List URL: http://api.rottentomatoes.com/api/public/v1.0/lists/movies/{{LIST}}.json?apikey=xxxx
Detail URL: http://api.rottentomatoes.com/api/public/v1.0/movies/{{ID}}.json?apikey=xxxx

Where {{LIST}} could be "box_office", "in_theaters", "opening", etc and the {{ID}} represents a specific movie id from the list. In my original code, I wrote the following code to enable pointing to the correct resource end-points:


MoveCollection = Backbone.Collection.extend({

   model: MovieModel,
   apikey: 'xxx', 
   url: 'http://api.rottentomatoes.com/api/public/v1.0/',

   load: function ( dbd ) {

      var db = dbd || 'box_office';
      
      /* Build the URL as part of the fetch */
      return this.fetch({ url: _.result(this, 'url') + 'lists/movies/' + db + '.json?apikey=' + this.apikey });


   }

});

var MovieModel = Backbone.Model.extend({
   
   /* Use a different URL but base it on the one defined in the collection */
   url: function () {
         return _.result(this.collection, 'url')+'movies/'+this.id+'.json?apikey='+this.collection.apikey;
   },

});



As of Backbone 1.0.0, the url option in the fetch() call will be forced onto the model's url property. That might be great in many cases but was not going to work for me. If you want to avoid this side-effect and remain in control of the URL settings, you have to avoid passing the url option into the fetch call. As a result, I refactored the code to construct the collection's URL using a function:


MoveCollection = Backbone.Collection.extend({

   model: MovieModel,

   urlRoot: 'http://api.rottentomatoes.com/api/public/v1.0/',
   apikey: 'xxx', 
   listSrc: 'box_office',

   url: function () {
      return _.result(this, 'urlRoot') + 'lists/movies/' + this.listSrc + '.json?apikey=' + this.apikey;
   },

   load: function ( dbd ) {

      this.listSrc = lsrc || 'box_office';
      
      /* Allow the url function to build the url used in the fetch */
      return this.fetch();

   }

});

var MovieModel = Backbone.Model.extend({
   
   /* The urlRoot is now the common path between the collection and model */
   url: function () {
      return _.result(this.collection, 'urlRoot')+'movies/'+this.id+'.json?apikey='+this.collection.apikey;
   },

});



Now, I dynamically build the URL based on several option set on the collection and just change the source in the load() function. This probably is a better way to do this anyways so the change in the library simply made me address and fix it.