Implementing a public API in Rails 3
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[:id])
respond_with(@product) do |format|
format.js { render :json => @product, :callback => params[:callback] }
end
end
end
Let’s see what we’ve changed:
- Tell Rails which formats we’ll be responding to (line 2)
-
respond_with(@product)
because Rails can tell which format it should return according to the requested format (line 7) -
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.