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 3)
- respond_with(@product) because Rails can tell which format it should return according to the requested format (line 8)
- Tell it to use a callback for the :js format, because that one is bit trickier (i.e. non standard) (line 9)
And that is how you can expose your Rails API to other domains. The next post covers consuming such an API.
Quick and concise. I like it.
How would you go about protecting the API with a API key of some sort ?
Thanks.
Hi,
I would use Devise‘s token authentication, simply because I tend to use Devise as the authentication mechanism.
There is a good example of writing an authenticated (and versioned) API in the “Rails 3 in Action” book, which I’ve reviewed.
This is a very nice easy to read article. Couple of questions for you…
1. Why use .js over .jsonp for the format type as that is typically the command used to make the cross site call.
2. Is there any easy way to generate user documentation for your REST endpoints that your public can view? Preferably not just the rake routes output.
a) Ideally a way to use something like this? https://github.com/wordnik/swagger-ui
Hi Chris,
1. I chose the .js extension because we’ll be returning actual Javascript code. But you could also pick the jsonp extension : as long as your users call the correct URI, it should work the same.
2. Sadly, I have no idea, but if you come across something I’d be very interested. But at the same time (and in my humble opinion), API documentation goes beyond specifying endpoints and arguments : you need examples using your API, etc.
Oh this saved me!! There are several posts about using JSONP, but this is the only place that specified to return the callback parameter as part of the response. Thanks so much.
Glad I could save you some time. I had to do some digging the first time I tried to get this working…
Can I just do something like “POST”, or “PUT” from client to my rails_server?
I’m not sure I understand your question, but yes, you can use POST and PUT in an API. But you still need to process those cases on the server and return an appropriate response to the client (e.g. success, error, etc.).
Hey David,
many many thanks for this tutorial. I am looking around to solve this problem for over a week now and didn’t find a proper well-explained tutorial, yet – until I stumbled onto your site!
Cheers
Jens
Glad to hear it, Jens ! I hope I saved you some time…