.

Coffee Powered

code and content

Powerful, easy, DRY, multi-format REST APIs

Rails’ baked-in REST support is great. Build your app right, and you can expose a programmatic interface to your users for free.

That said, many times providing views in non-HTML formats tends to be bulky and unwieldy. You end up with either very brittle representations of your data, or extremely bulky respond_to blocks in your controllers.

Fortunately, there’s a better way! We’re going to provide two new render targets, :to_yaml and :to_json which will let us write a single XML builder view, and then provide that view in XML, YAML, and JSON formats according to the consuming developer’s preferences.

In application.rb you’ll want to override the render method.

def render(opts = {}, &block)
  if opts[:to_yaml] then
    headers["Content-Type"] = "text/plain;"
    render :text => Hash.from_xml(render_to_string(:template => opts[:to_yaml], :layout => false)).to_yaml, :layout => false
  elsif opts[:to_json] then
    content = Hash.from_xml(render_to_string(:template => opts[:to_json], :layout => false)).to_json
    cbparam = params[:callback] || params[:jsonp]
    content = "#{cbparam}(#{content})" unless cbparam.blank?
    render :json => content, :layout => false
  else
    super opts, &block
  end
end

As you can see, we render a single XML view, and then load it to a hash from XML, and use Rails’ built-in Hash#to_json and Hash#to_yaml methods to provide the data in the desired format. There is a single glaring problem with this approach, though - Hash#from_xml is dog slow because it uses REXML. There’s a fantastic solution, though!

Courtesy of a blog post over at cobravsmongoose, we have a libxml drop-in for Hash#from_xml

First, install libxml and then faster_xml_simple.

Second, include a monkeypatch to Hash#from_xml with the following:

require 'faster_xml_simple'
class Hash
  def self.from_xml(xml)
    undasherize_keys(typecast_xml_value(FasterXmlSimple.xml_in(xml,
      'forcearray'   => false,
      'forcecontent' => true,
      'keeproot'     => true,
      'contentkey'   => '__content__')
    ))
  end
end

You can run the benchmarks if you’d like, but it’s orders of magnitude faster than REXML. Seriously. Don’t use REXML. It’s like trying to run a Ferrari off of a 9-volt battery.

Now, let’s say you have an action you want to provide HTML, XML, JSON, and YAML views for.

def index
  ...
  respond_to do |wants|
    wants.html
    wants.xml  { render :layout => false }
    wants.json { render :to_json => "posts/index.xml.builder" }
    wants.yaml { render :to_yaml => "posts/index.xml.builder" }
  end
end

Finally, throw together your index.xml.builder file as you best see fit.

xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
xml.posts do
  @posts.each do |post|
    xml.post(:id => post.id) do
      xml.user(:id => post.user.id) +
      xml.content do
        post.post_body
      end
    end
  end
end

And all of a sudden, bam! You’ve got your posts available in HTML…

/posts/index

…and in XML, YAML, and JSON, along with the associated User. By using an XML builder, you can make the serialized data as complex and customized as you’d like. No more funky respond_to blocks, no more exposing data you don’t want to. Expose what you want, and just what you want, in several formats.


/posts/index.xml
/posts/index.yml
/posts/index.json

One final trick is that the JSON views accept an optional callback or jsonp parameter, which will cause the content to be passed to a Javascript function matching the passed parameter, as per the JSONP spec.

For example, if you have a /foo/bar.json view that would render the following JSON:

"{\"foo\":\"bar\"}"

Calling /foo/bar.json?jsonp=returnFunc would return the following:

returnFunc("{\"foo\":\"bar\"}")

Check out the JSONP spec for more.

12 Comments

  1. Chris Eppstein
    September 28, 2008 at 1:05 am | Permalink

    I think it would be cleaner to augment render to allow you to pass a render :format => :xml for the json and yaml responses and then to use an after_filter to convert the response to the correct format and mime-type.

  2. September 28, 2008 at 10:29 am | Permalink

    Very interesting approach, Chris. That would help clean up the respond_to blocks, as well!

  3. September 30, 2008 at 1:57 am | Permalink

    I think the guys at boomloop/playtype did a good write up on why the out-of-the-box rails approach is wrong for a few reasons: http://playtype.net/past/2008/5/20/rest_on_rails_filling_in/

  4. September 30, 2008 at 7:29 pm | Permalink

    I completely agree, Glenn. That was one of my primary reasons for this approach. I’d first tried to implement a fancy ActiveRecord plugin that defined a to_xml schema for models, but that turned out to be both cumbersome, and the Wrong Way(TM) to do it.

    I’ve found this approach to be extremely flexible, letting me expose only data I want to, in the formats I want to, without requiring special gymnastics to make it work.

  5. October 3, 2008 at 1:46 pm | Permalink

    Excellent - I love this idea. My only minor quibble is that it’s a little confusing having an xml builder in the JSON renderer. I would prefer an abstract structure builder (index.structure.builder) that would be semantically clearer.

  6. October 14, 2008 at 6:40 pm | Permalink

    I really think this is overcomplicating a simple problem.

    For JSON and YAML, all you need to do is define a method on the model you want to expose, like so:

    
    class Post < ActiveRecord::Base
      def to_json
        self.attributes.to_json
      end
      def to_yaml
        self.attributes.to_yaml
      end
    end
    

    Of course, if you want, you define a method like so:

    
    def public_attributes
      self.attributes.reject do |(attribute, value)|
        not %w(user_id, title, body, created_at).include?(attribute.to_s)
      end
    end
    

    Then you can have:

    
    def to_json
      self.public_attributes.to_json
    end
    

    That means no code in the controller… much cleaner.

    Also provides you a little hook to even add computed attributes if you like, or include even a user’s attributes instead of a user ID. Whatever.

    Much cleaner in my opinion.

  7. October 14, 2008 at 6:53 pm | Permalink

    Sorry for the lack of format… I don’t really know what formatting options there are with WordPress.

  8. October 14, 2008 at 7:06 pm | Permalink

    My issue with tying to_json/to_yaml to the model is that it presumes that I will always want the same representation of the model everywhere I want to express that model in a serialized format. It’s a fairly brittle solution, though certainly a quick and easy one. I found that often times, I needed to have a lot of control over the data that was being exposed in these formats, and doing so required jumping through all kinds of hoops, either in the controller or in the model, when what I was really doing was trying to express a specific view for the model.

    In the MVC architecture, the model should simply present its data to the view, and the view should decide how to present that data to the user; placing ownership of the model’s displayed format in the model’s hands violates MVC, and makes using it somewhat more brittle.

  9. October 14, 2008 at 7:16 pm | Permalink

    But here you do end up with misrepresentation and a bit more explicit definition where it’s not exactly necessary. So you want multiple attribute profiles (for a lack of a better term)? Just have different methods that respond with hashes with the right data…

    
    def public_timeline_attributes
      self.attributes.reject do |attribute, value|
        not %w(id title created_at udated_at).include?(attribute.to_s)
      end
    end
    

    Of course, you’d abstract it out to something where you can just name which parameters to expose, and have it always respond with a Hash. This way calling #to_json or #to_yaml on it always gets what you want.

    There may be better solutions but I think putting attribute publicity is something that the model should concern itself with, especially when you can keep the view very simple and clean (and not requiring transforming to XML then back again). It makes changes in the model site-wide and maintenance of those views not necessary.

  10. October 14, 2008 at 8:01 pm | Permalink

    The abstracted method that lets me pass a list of desired attributes and get back a hash is the method I’d originally gone with in a recent app…until I realized that when I wanted to construct complex views of the data, I was passing in massive multi-level hashes that defined a hierarchy of desired attributes across a series of models that were effectively doing the same thing that an XML builder did for me.

    In simple cases, some kind of “request attributes X, Y, and Z, get a hash back” on the model works great. What I’ve found is that by allowing each view to decide what data is relevant, I can expose more or less data for each API call, with the intent of reducing the number of followup calls that a developer would have to make in order to get all the data they want, without necessarily bloating the response results by throwing in everything and the kitchen sink.

    If I have topics -> posts -> users, and I request a list of a user’s posts in a serialized format, I may want something like:

    user {
      id
      name
      picture
      posts {
        post body
        post date
        post link
        owning thread {
          title
          link
        }
      }
    }

    However, if I want to get a list of posts in a thread, the view may need to be something like:

    
    thread {
      title
      link
      posts {
        post body
        post date
        user {
          id
          name
          picture
        }
      }
    }
    

    Two very different views. To do those in the model, I’d need something like

    
    @user.for_serialization(:id, :name, :picture, [:posts => {:body, :date, :link, [:thread => {:title, :link}]}]).to_json
    
    @thread.for_serialization(:title, :link, [:posts => {:body, :date, [:users => {:id, :name, :picture}]}]).to_json
    

    Oof. Starting to get a little heavy, and that’s both assuming a for_serialization that accepts nested attributes like that, and a relatively simple example.

    If you want to get more complex and start including data that’s not necessarily available to the model, like, say, links generated via your routing, then you have to start passing procs into your for_serialization. It becomes a giant unholy mess.

    I did initially go the route of an ActiveRecord extension which provided super-customizable serialization calls, with nested associations and procs and the like, and it was just incredibly unwieldy to use in practicality.

  11. Alexwebmaster
    March 3, 2009 at 5:47 am | Permalink

    Hello webmaster
    I would like to share with you a link to your site
    write me here preonrelt@mail.ru

  12. April 21, 2009 at 11:40 am | Permalink

    It took me two hours after reading your article to get JSONP to work, I was missing adding an explicit :callback parameter!

    format.json { render :json => @artists.to_json(), :callback => params[:callback] }

3 Trackbacks

  1. [...] Coffee Powered » Powerful, easy, DRY, multi-format REST APIs [...]

  2. From vBharat.com » Powerful, easy, DRY, multi-format REST APIs…

    Rails’ baked-in REST support is great. Build your app right, and you can expose a programmatic interface to your users for free….

  3. [...] means that the faster_xml_simple monkeypatch is no longer needed. I don’t think we’re doing much else with XML on blippr, but [...]

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*