Presenting the #blue api
In building #blue (sign up now!), one of the problems we faced was how to build json
data in response to requests to our API. The typical rails solution would be to override #as_json
in a model class, then write a controller like this:
class ContactsController < ApiController
responds_to :json
def show
respond_with Contact.find(params[:id])
end
end
I always prefer to keep my controllers as skinny as possible, so this looks like a great solution. The respond_with
call takes care of converting the message to json and responding with the right Content-Type
, all in a simple call. However it has a number of problems and disadvantages.
The biggest issue for our API is that rather than expose the id
of each model, we’ve tried to encourage the use of the uri
instead. So the json
returned for a single contact (for example) looks like this:
{
"contact": {
"uri": "https://api.example.com/contacts/ccpwjc",
"name": "George",
"email": "george@handmade.org",
"msisdn": "447897897899",
"phone_number": "07897897899",
"messages": "https://api.example.com/contacts/ccpwjc/messages"
}
}
It doesn’t just have a uri
for the actual contact, but also for the messages belonging to that contact (and yes, I regret not calling that attribute messages_uri
). Models can’t generate uris, and shouldn’t really be aware of them, so overriding #as_json
doesn’t work. In any case, the json structure is really presentation logic, not business logic. It doesn’t belong in the model.
Presenting a single model
The solution we’ve used is to build a presenter for each model, solely responsible for building the json. Here’s an example for a contact:
class ContactPresenter
include Rails.application.routes.url_helpers
attr_accessor :controller, :subject
delegate :params, :url_options, :to => :controller
delegate :errors, :to => :subject
def initialize(controller, subject)
@controller = controller
@subject = subject
end
def as_json(options = {})
{:contact => {
:uri => uri,
:email => subject.email,
:name => subject.name,
:msisdn => subject.msisdn,
:phone_number => subject.phone_number,
:messages => api_contact_messages_url(:contact_id => subject.id)
}
end
def uri
api_contact_url(:id => subject.id)
end
end
It’s now simple to rewrite our controller to use the new presenter:
class ContactsController < ApiController
responds_to :json
def show
respond_with ContactPresenter.new(self, Contact.find(params[:id]))
end
end
Presenting pages of models
The presenter above works well for a single model, but many of our API calls return a page of results. The /contacts for example returns all the contacts belonging to a user (of which there may be hundreds). Luckily it’s simple to adapt this pattern to present pages like this. First, we change our original #as_json
method slightly:
def as_json(options = {})
if options[:partial]
{
:uri => uri,
:email => subject.email,
:name => subject.name,
:msisdn => subject.msisdn,
:phone_number => subject.phone_number,
:messages => api_contact_messages_url(:contact_id => subject.id)
}
else
{:contact => as_json(:partial => true)}
end
end
This change allows us to call as_json with the options :partial
. With the option, a hash of data is returned. Without, the same hash is returned, wrapped in another hash.
Next, add a page presenter:
class ContactPagePresenter
include Rails.application.routes.url_helpers
attr_accessor :controller, :subject
delegate :params, :url_options, :to => :controller
delegate :errors, :to => :subject
def initialize(controller, subject)
@controller = controller
@subject = subject
end
def as_json(options = {})
contacts = subject.map {|o| ContactPresenter.new(controller, o).as_json(:partial => true) }
{:contacts => contacts}.tap do |result|
if subject.previous_page
result[:previous_page_uri] = contacts_url(subject.previous_page)
end
if subject.next_page
result[:next_page_uri] = contacts_url(subject.next_page)
end
end
end
end
Finally, we can add an index action using this presenter:
class ContactsController < ApiController
responds_to :json
def index
respond_with ContactPagePresenter.new(self, Contact.paginate(
:page => params[:page],
:per_page => 50)
)
end
end
Refactoring common logic
The code above is a very much simplified version of what we do in #blue. We have many controllers, and several different models, so in our actual code we’ve abstracted out as much common logic as possible. In reality, our contacts controller looks more like this:
class ContactsController < ApiController
before_filter :find_contact, :only => [:show, :update]
def show
present @contact
end
def index
present_page_of current_account.contacts
end
def create
@contact = current_account.contacts.build(attributes)
@contact.save
present @contact
end
def update
@contact.update_attributes(attributes)
present @contact
end
private
def find_contact
@contact = current_account.contacts.where(:_id => params[:id]).first
head :status => :not_found unless @contact
end
end
I think the code looks pretty clean. The clever stuff happens in the #present
and #present_page_of
methods, defined in the superclass:
class ApiController < ApplicationController::Base
protected
def present(instance, options = {})
presenter = presenter_class.new(self, instance)
options[:location] ||= presenter.uri if request.post? && subject.errors.empty?
respond_with presenter, options
end
def present_page_of(collection, options = {})
presenter = page_presenter_class.new(self, page_of(collection))
respond_with presenter, options
end
def page_of(collection)
collection.paginate(:page => params[:page], :per_page => 50)
end
def presenter_class
(self.class.name.gsub!("Controller", "").singularize + "Presenter").constantize
end
def page_presenter_class
(self.class.name.gsub!("Controller", "").singularize + "PagePresenter").constantize
end
end
The #present
and #present_page_of
methods handle determining the correct presenter to us, as well as paginating the collection where required. They still use rails build in #respond_with
method, which helps provide the correct response headers for each request. As the ContactPresenter delegates #errors
to its subject, if there are validation errors, #respond_with
correctly returns a 422.
One further motivation for this pattern (other than moving presentation logic out of the model) is that should we want to release a new version of our API, we’ll be able to get a lot of the way there simply by swapping which presenter is used. We started using this code about 8 months ago, and I’m still pretty happy with it. I hope you find something useful in it too.
Any comments or suggestions, please get in touch with me on twitter.