Tribesports Tech Blog

25 press-ups for breaking the build...

Separating API Logic With Decorators

| Comments

In our last post we demonstrated how to use Rails 3’s response and rendering system to provide a cleanly-versioned API. By defining a custom MIME type and a renderer for that type, we were able to version our API, and handle responses without any changes to our controllers.

However, we were left with unsightly #to_api_v1 methods on all of our business models. We’d like to separate presentation logic from our business logic, and to do that we’re going to use decorators. We’re making use of Jeff Casimir’s draper gem, which provides some nice convenience methods for decorating Rails models.

We start by creating a namespaced decorator for our Activity model, and move the Activity#to_api_v1 method into it:

app/decorators/api_v1/activity_decorator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
module API_V1
  class ActivityDecorator < Draper::Base
    decorates :activity

    def to_api_v1(options = {})
      {
        id: model.id,
        content: model.content,
        creator_name: model.creator_name
      }.to_json(options)
    end
  end
end

This is much better - our presentation logic is out of our model. However, we’ve created a problem: Rails’ default Responder was relying on the Activity#to_api_v1 method being present to detect whether it was capable of rendering an Activity instance in our API format. We need to create a custom responder that will also render a resource if an appropriate Decorator can be found.

The responder uses the method #resourceful? to determine whether a resource can be rendered in the given format (we have no idea what the MacGuyverish name is about). We can override this method:

1
2
3
4
5
6
7
8
9
10
11
12
# app/responders/application_responder.rb
class ApplicationResponder < ApplicationController::Responder
  def resourceful?
    super || ApplicationDecorator.has_decorator_for?(resource, format)
  end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  self.responder = ApplicationResponder
  # ...
end

This makes use of some decorator lookup methods that we provide in our application’s base decorator class:

app/decorators/application_decorator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ApplicationDecorator < Draper::Base

  def self.has_decorator_for?(resource, format)
    true if decorator_class_for(resource, format)
  end

  def self.decorator_for(resource, format)
    if decorator_class = decorator_class_for(resource_format)
      decorator_class.decorate(resource)
    end
  end

  private

  # (<#Activity id: 1>, :api_v1) => API_V1::ActivityDecorator
  def self.decorator_class_for(resource, format)
    namespace_for(format).const_get(decorator_name_for(resource))
  rescue NameError => e
    raise if e.is_a?(NoMethodError)
    nil
  end

  def self.namespace_for(format)
    Object.const_get(format.to_s.upcase)
  end

  def self.decorator_name_for(resource)
    resource.class.name + "Decorator"
  end

end

This means that if we’re consistent with our decorator naming, we can look up the appropriate decorator according to resource class and format. The above is slightly simplified - there are extra cases required if the resource is an array or an ActiveRecord::Relation (as you might see in index actions), but these aren’t particularly interesting.

Now we have a custom responder that will handle decorated resources properly; we have a decorator lookup system that will return an appropriately-decorated resource according to the requested format; and we have the decorator itself. All we need to do now is modify our renderer so that it will decorate a resource appropriately before rendering it.

app/renderers/api_v1_renderer.rb
1
2
3
4
5
ActionController::Renderers.add(:api_v1) do |resource, options|
  self.content_type = Mime::API_V1
  decorated_resource = ApplicationDecorator.decorator_for(resource, :api_v1) || resource
  render options.merge(:json => decorated_resource)
end

This is largely self-explanatory; the main point to note is that if the renderer can’t find an appropriate decorator, it will fall back on the default json rendering. Isn’t that what we wanted to avoid? Yes, but with a caveat: if a resource has errors (for example, after an invalid update), the responder doesn’t render the resource itself; it renders the array of errors. In this case, then, for the time being we’re just going to send the JSON representation of that array.

Finally, since we’re using the built-in JSON renderer to actually create the output (it’s JSON we’re sending, after all), we modify our decorators to respond to #as_json instead of #to_api_v1:

app/decorators/api_v1/activity_decorator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
module API_V1
  class ActivityDecorator < ApplicationDecorator
    decorates :activity

    def as_json(options = {})
      {
        id: model.id,
        content: model.content,
        creator_name: model.creator_name
      }
    end
  end
end

And that’s it! To recap, we have:

  1. Registered a custom MIME type to inform Rails about our API format,
  2. Written decorators to contain the API presentation logic,
  3. Built a lookup system to locate the format-specific decorator for each resource,
  4. Created a custom responder to let Rails know what resources can be renderered, and
  5. Created a custom renderer that first decorates our resources, then renders them.

As a result, we’re able to keep our API presentation logic completely separate, organised using version-specific namespaces. Adding a new controller action to our API is as simple as writing respond_to :api_v1 in the appropriate controller, and ensuring that a correctly-named decorator exists for that resource type. To create a second version of the API, we just define a new MIME type, renderer and add the appropriate decorators. And all of this means that our controller actions themselves require no extra code at all.

We’ve achieved all of our original goals, and with only a few tens of lines of code. Time for a particularly smug cup of tea; the Tribesports office favours Oolong for this purpose. Lapsang Souchong is alleged by some to be smugger still, but it tastes like boiled furniture.

Versioning the Tribesports API

| Comments

As Andrew is hard at work on the upcoming Tribesports iPhone app, we’ve been paying a bit of attention to our API. Where previously we had responded to JSON requests in a rather ad-hoc fashion according to our application’s internal needs, a full-blown API requires a more systematic approach.

Our goals are as follows:

  1. Allow clean API versioning
  2. Separate API presentation logic from business models
  3. Minimise additional controller complexity

Like all good REST disciples, we know that adding versioning information to URLs will cause the breakdown of civil society, so we define a custom MIME type for use in the Accept header of requests:

config/initializers/mime_types.rb
1
Mime::Type.register "application/vnd.tribesports.api.v1+json", :api_v1

This means Rails will now treat our :api_v1 format as a first-class citizen, so we can use it in controller response blocks, like so:

app/controllers/activities_controller.rb
1
2
3
4
5
6
7
8
9
10
class ActivitiesController < ApplicationController
  respond_to :html, :api_v1

  def show
    @activity = Activity.find(params[:activity_id])
    respond_with(@activity) do |format|
      format.api_v1 { render :text => @activity.to_json }
    end
  end
end

Already, then, we’re able to recognise and respond appropriately to requests for our custom MIME type. There are two problems here, however. Firstly, we’re left with controller-level boilerplate that must be replicated in every method that responds to API calls. And secondly, we’re falling back on our business model’s default #as_json method, which will render the entire object. We could override it, or pass in options, but the former conflicts with our versioning requirement while the latter would spread presentation logic throughout our controllers.

To address these problems, we look to Rails 3’s responders and renderers. If we don’t supply a format.api_v1 block in the response block, Rails’ default Responder will attempt to intelligently render the resource in the given format. The logic for a simple show action varies depending on the format type, and runs roughly thus for a given resource:

  • Navigational formats (e.g. HTML):

    Render using the appropriate template (e.g. 'activities/show.html.haml')

  • API formats (everything else):

    If the resource responds to a #to_#{format} method, and a renderer exists for the format, then use the renderer; otherwise, attempt to render using an appropriate template.

For update and create methods there is additional logic to deal with redirection and error handling, but this represents the basics.

To make our custom format work properly with the responder logic, we therefore need to define a #to_api_v1 method on all our models, and define a custom renderer so Rails will know what to actually do with this method.

Let’s have a look at the built-in JSON renderer:

actionpack/lib/action_controller/metal/renderers.rb
1
2
3
4
5
6
7
8
9
10
11
12
module ActionController
  module Renderers
    # ...

    add :json do |json, options|
      json = json.to_json(options) unless json.kind_of?(String)
      json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
      self.content_type ||= Mime::JSON
      json
    end
  end
end

We can see the renderer is passed the resource (potentially as an already-JSON-encoded string) and a set of options. If the resource isn’t already encoded, the renderer encodes it using the supplied options. It then wraps it in an optional callback, sets the response MIME type, and returns the output. So our custom renderer might appear as follows:

1
2
3
4
5
6
7
8
9
10
11
12
# app/renderers/api_v1_renderer.rb 
ActionController::Renderers.add :api_v1 do |resource, options|
  self.content_type = Mime::API_V1
  self.response_body = resource.to_api_v1(options)
end

# app/controllers/application_controller.rb
require_relative '../renderers/api_v1_renderer'

class ApplicationController < ActionController::Base
  #...
end

Now all we have to do is define #to_api_v1 on every model we want to represent in the API (yes, yes, we know - a horrible mixing of concerns. More on that later.):

app/models/activity.rb
1
2
3
4
5
6
7
8
9
10
11
class Activity
  #...

  def to_api_v1(options)
    {
      id: id,
      content: content,
      creator_name: creator.name
    }.to_json(options)
  end
end

As a result of all that, there’s no longer any need for the custom response block in our controller methods:

app/controllers/activities_controller.rb
1
2
3
4
5
6
7
8
class ActivitiesController < ApplicationController
  respond_to :html, :api_v1

  def show
    @activity = Activity.find(params[:activity_id])
    respond_with(@activity)
  end
end

We can now send a request using our custom MIME type in the Accept header, and receive an appropriate response.

So far, so good. We’ve got the Rails responder doing all our heavy lifting, our controllers are clean, and we have versioned resource presentation. That’s two of our goals accomplished, but we’ve still got presentation logic cluttering up our business models. To address that we’ll be using Decorators, which will be the subject of our next blog post.