Implementing a public API in Rails 3

Posted on April 10, 2011

As you’re probably aware, Rails helps out a lot when using generators: in addition to generating the views, it also provides XML access to data. What we’ll cover today is implementing a basic public API that can be consumed from outside our app (i.e. with cross-domain requests).

Getting started

Let’s start by generating a trivial product scaffold with

  
  
    
    rails g scaffold product reference:string quantity:integer
  
    
  

After migrating the DB and creating a product, you can see the data in XML format at http://localhost:3000/products/1.xml. That is because rails has already exposed the data in XML format for you:

  
  
    def show
  @product = Product.find(params[:id])

  respond_to do |format|
    format.html # show.html.erb
    format.xml  { render :xml => @product }
  end
end
  
  

As you can see, the respond_to block accepts the XML format and will return an XML representation of the product instance.

Introducing JSON

Although AJAX stands for “asynchronous JavaScript and XML”, the cool Web 2.0 kids now use JSON instead of XML, due to its lower overhead. In other words, since JSON can send data more concisely, it require less bandwidth to do so, and the response will return quicker.

Let’s make the show action return JSON:

  
  
    def show
  @product = Product.find(params[:id])
 
  respond_to do |format|
    format.html # show.html.erb
    format.json  { render :json => @product }
    format.xml  { render :xml => @product }
  end
end
  
  

If you now go to http://localhost:3000/products/1.json, the app will return a JSON representation of the product instance. It will even use the correct content-type of application/json (per RFC 4627), which is why your browser might prompt you to download it instead of rendering it inline.

But since our goal was to make this API publicly available across domains, we’re not quite done yet.

Problem?

Per wikipedia,

Under the same origin policy, a web page served from server1.example.com cannot normally connect to or communicate with a server other than server1.example.com.

And that is precisely the issue we’ll run into when trying to get data from our API from another domain: the response will be empty (and the request will show up in red in Firebug).

How can this be solved? Instead of requesting simple JSON, we’re going to request arbitrary JavaScript code that will contain the data we’re after. This technique is called JSON with padding (or JSONP).

JSONP in Rails

In addition to responding to JSON, we’ll have our app respond to the *.js format and return JavaScript. So if we receive a request with a callback function, we’ll wrap the JSON in the callback and return the JavaScript to the caller:

  
  
    def show
  @product = Product.find(params[:id])
 
  respond_to do |format|
    format.html # show.html.erb
    format.js  { render :json => @product, :callback => params[:callback] }
    format.json  { render :json => @product }
    format.xml  { render :xml => @product }
  end
end
  
  

So if you now navigate to http://localhost:3000/products/1.js, you should see the JSON that will be returned (with a content type of application/javascript). But if you got to http://localhost:3000/products/1.js?callback=magic, you’ll notice the JSON has been wrapped in the callback method we’ve provided.

This way, external applications can provide a callback method and consume our public API provided by our Rails app even with cross-domain requests. A subsequent post covers consuming the API using jQuery.

Cleanup

Rails provides built-in functionality to clean up our code, so let’s do that:

  
  
    class ProductsController < ApplicationController
  respond_to :html, :xml, :json, :js
 
  def show
    @product = Product.find(params&#91;:id&#93;)
 
    respond_with(@product) do |format|
      format.js  { render :json => @product, :callback => params[:callback] }
    end
 
  end
end
  
  

Let’s see what we’ve changed:

  1. Tell Rails which formats we’ll be responding to (line 2)
  2. respond_with(@product) because Rails can tell which format it should return according to the requested format (line 7)
  3. Tell it to use a callback for the :js format , because that one is bit trickier (i.e., non standard) (line 8)

And that is how you can expose your Rails API to other domains. The next post covers consuming such an API.


Would you like to see more Elixir content like this? Sign up to my mailing list so I can gauge how much interest there is in this type of content.