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?

How the same origin policy feels about cross-domain requests.

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:

  1. Tell Rails which formats we’ll be responding to (line 3)
  2. respond_with(@product) because Rails can tell which format it should return according to the requested format (line 8)
  3. 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.

This entry was posted in Rails. Bookmark the permalink.

10 Responses to Implementing a public API in Rails 3

  1. Cac3a says:

    Quick and concise. I like it.

    How would you go about protecting the API with a API key of some sort ?

    Thanks.

  2. david says:

    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.

  3. Chris says:

    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

  4. david says:

    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.

  5. Ed Taupier says:

    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.

  6. david says:

    Glad I could save you some time. I had to do some digging the first time I tried to get this working…

  7. Joe says:

    Can I just do something like “POST”, or “PUT” from client to my rails_server?

  8. david says:

    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.).

  9. Jens says:

    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

  10. david says:

    Glad to hear it, Jens ! I hope I saved you some time…