GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Backbone.js, JSON API, and Relational Data: A Primer

While following patterns suggested by JSON API with Ember may be straightforward, little documentation exists for its usage within Backbone.js. With feed-readers popular in the past few months, I've decided to demonstrate how to follow the specification defined by JSON API to provide data to a Backbone application; namely, how to populate collections and models with associated data.

The Application API

Let's start with the Rails application's API implementation, which is fairly straightforward:

# app/controllers/api/feeds_controller.rb
class Api::FeedsController < ApplicationController
  respond_to :json

  def index
    feeds = Feed.rss
    respond_with feeds, each_serializer: FeedSerializer
  end

  def show
    feeds = Feed.where(id: params[:id]).rss
    respond_with feeds, each_serializer: FeedSerializer
  end
end

The controller is using ActiveModel::Serializers to generate JSON responses for both Feed and Entry objects.

# app/serializers/feed_serializer.rb
class FeedSerializer < ActiveModel::Serializer
  attributes :id, :title, :url, :feed_url, :etag, :description

  has_many :entries, serializer: EntrySerializer,
    embed: :ids, include: true, key: :entries
end

# app/serializers/entry_serializer.rb
class EntrySerializer < ActiveModel::Serializer
  attributes :id, :url, :title, :published, :author, :content

  def content
    object.content || object.summary
  end
end

These classes result in a JSON structure like this when retrieving api/feeds.json:

{
  "entries"=> [
    {"id"=>"a9f9283c34af5c6f7ab6fc461e5ba8ee",
      "url"=> "",
      "title"=> "",
      "published"=>"2013-09-22T18:25:02Z",
      "author"=>"",
      "content"=>""},
    {"id"=>"e805ed779257cd0d1f69b747fb01b591",
      "url"=> "",
      "title"=>"",
      "published"=>"2013-09-22T18:16:06Z",
      "author"=>"",
      "content"=> ""},
    {"id"=>"6601a53c98e690a46b8bbeca46a6904a",
      "url"=>"",
      "title"=>"",
      "published"=>"2013-08-30T15:16:03Z",
      "author"=>"",
      "content"=> ""}
  ],
  "feeds"=> [
    {"id"=>"e74786582200ea744c0d0a72fc616199",
      "title"=>"",
      "url"=>"",
      "feed_url"=>"",
      "etag"=>"\"4762e3773a5404de50dcfaef49697460\"",
      "description"=>"",
      "entries"=> ["a9f9283c34af5c6f7ab6fc461e5ba8ee", "e805ed779257cd0d1f69b747fb01b591"]},
    {"id"=>"1a33061dccfdc80aff5ce04df9591f6e",
      "title"=>"",
      "url"=>"",
      "feed_url"=>"",
      "etag"=>"\"7896d-eaef-4e6c07d3b7300\"",
      "description"=> "",
      "entries" => ["6601a53c98e690a46b8bbeca46a6904a"]}
  ]
}

There are two keys, feeds and entries, returned. Each item within feeds maintains a reference to its list of entry IDs, which will need to be associated when building up the structure within the Backbone collection.

The Backbone Application

Let's look at the Backbone collection:

# app/assets/javascripts/collections/feeds.coffee
class @App.Collections.Feeds extends Backbone.Collection
  model: App.Models.Feed
  url: '/api/feeds'
  parse: (response) =>
    @entries = response.entries
    response.feeds

parse is doing most of the heavy lifting here, assigning a property entries to the collection (to be used later) and returning response.feeds, which gets assigned internally to models().

With the collection of feeds knowing about entries, it becomes the responsibility of each App.Models.Feed to correctly reference each App.Models.Entry:

# app/assets/javascripts/models/feed.coffee
class @App.Models.Feed extends Backbone.RelationalModel
  relations: [
    {
      type: Backbone.HasMany
      relatedModel: 'App.Models.Entry'
      collectionType: 'App.Collections.Entries'
      key: 'entries'
    }
  ]

  initialize: ->
    @set 'entries', _.filter @collection.entries, (entry) =>
      _.indexOf(@get('entries'), entry.id) >= 0

  title: ->
    @get 'title'

  description: ->
    @get 'description'

  entries: ->
    @get 'entries'

Within App.Models.Feed's initialize, we bootstrap the JSON data from the collection's entries into the entries attribute on the App.Models.Feed model. Because the entries array may contain IDs of entries for other feeds, however, we filter out everything that doesn't belong. Once the records have been filtered correctly, Backbone-relational.js handles the rest.

For reference, here are App.Models.Entry and App.Collections.Entries:

# app/assets/javascripts/models/entry.coffee
class @FeedMe.Models.Entry extends Backbone.RelationalModel
  title: ->
    @get 'title'

  url: ->
    @get 'url'

  published: ->
    @get 'published'

  content: ->
    @get 'content'

  author: ->
    @get 'author'

# app/assets/javascripts/collections/entries.coffee
class @FeedMe.Collections.Entries extends Backbone.Collection
  model: FeedMe.Models.Entry

Results

While there is a bit of wiring involved, it's fairly easy to set up a Backbone app to interact with associated data provided by an API following the patterns of JSON API. If you're using Marionette.js to handle associated data and have examples to share, tweet @thoughtbot to let us know!

Learn More About Backbone.js

If you're interested in learning how to build Backbone apps, I recommend you check out thoughtbot's book Backbone.js on Rails, which includes an example app in addition to the book itself.

Go grab a copy today!

Additionally, if you're a Learn Prime subscriber, you'll get access to the book for free. Learn Prime starts at $29/month and grants subscribers access to our books, screencasts, exclusive subscriber content, and more!