Skip to content
Mike Bradford edited this page May 25, 2012 · 1 revision

It's in our controllers where the most API-specific functionality takes place. Let's take a look at some of the highlights from our ApplicationController.

see app/controllers/application_controller.rb

The first thing to point out is the removal of the protect_from_forgery declaration.

Second, let's check out our before filters. The first two check that requests are sent with an the expected request headers. I'm using the User-Agent header because it's an easy way to determine from what device platform the request originated. Notice the use of the head method. This just returns a response with headers only and no content. You could do a lot more (or nothing) with this information, but remember not to rely on the content of headers for anything mission-critical, as they can be spoofed. We require any requests that take a body (sent via POST or PUT) to set the Content-Type header to application/json.

We also need to set the content type of each response. This is because rabl, in being agnostic to the Rack-based framework, does not set the content type header of the response when rendering.

We will add the validate_auth_key method as a before filter on our controllers on demand. Here we just validate that the request is made with the custom X-Authentication-Key header whose value is the auth_key. If no Cook object is found with this auth_key, the filter terminates the request and responds with a 401 Unauthorized status.

It might be wise to refer to the value of HTTP_STATUS_CODES defined in the Rack::Util module to find the names of the standard codes Rack recognizes.

We need to be sure to gracefully handle all errors using the rescue_from class method. In this case, we'll just log the error and send render a friendly error message. It would be wise to use a third party system, such as Airbrake, to send a error notification.

One final thing to point out is the importance of using return in our filters and our method invoked by rescue_from so that the render chain is terminated. Otherwise, we risk creating a double render exception.

CookbooksController

see app/controllers/cookbooks_controller.rb

Listing resources

Our index action is pretty simple. From our before filter, we have a cook instance from which we display all the associated cookbooks. Since we're using implicit rendering with rabl, Rails knows to look for the template for this action in app/views/cookbooks/index.json.rabl.

Here's where we'll introduce rabl JSON rendering for the first time. We're rendering a collection of cookbooks each having a collection of recipes.

In our cookbook index template, we'll just render our collection of cookbooks:

see app/views/cookbooks/index.json.rabl

We make a call to collection and pass it our cookbooks array. Since we're going to create a show template for the cookbook resource, we'll just reuse that template using the extends call for each Cookbook instance.

see app/views/cookbooks/show.json.rabl

Using Rabl's DSL, we make a call to object which declares that the object of focus for this template is a Cookbook instance. We then list each of it's attributes. We also use the node call to add content that isn't an attribute of the model instance. Lastly, we'll use the extends call again to render the content of each recipe.

see app/views/recipes/show.json.rabl

Finally, we'll create the template for a recipe resource. This assumes that we'll treat the Recipe model as a resource in it's own controller. Otherwise, we would just render it as a child in the cookbook show template:

object @cookbook
attributes :name, :description, :updated_at

node(:submitted_by) { |cookbook| cookbook.cook.username }

child :recipes do
  attributes :name, :category, :ingredients, :direction, :updated_at
	
	node(:submitted_by) { |recipe| recipe.cook.username }

	node(:cookbook) { |recipe| recipe.cookbook.name if recipe.cookbook }
end

We can test this action on the command line using the curl command. Assuming we have the ID and auth_key of the user, we can use the following command:

curl -H 'Accept: application/json' -H 'X-Authentication-Key: TudE1T0Dx19wL82OAYTr22WmRQhdCz5Nt35Y9lc1' -H 'User-Agent: Recipes iPhone' \
 -G http://localhost:3000/cooks/1/cookbooks.json

We don't need to set the content type of the request here because our request has no body.

Creating a resource

When a resource is successfully created, we won't return any content. Instead we'll return a status of 201 (Created) and a location header consisting of the URL of the resource. In our mobile app, we'll extract and persist the ID of the resource to use in subsequent calls to the show, update, and destroy actions.

If the resource is not created due to validation errors then a status of 422 (Unprocessable entity) is returned and the content returned will be a JSON response whose root node is named errors and whose content is just a JSON representation of the errors attribute on the model.

curl -H 'Content-Type: application/json' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'X-Authentication-Key: TudE1T0Dx19wL82OAYTr22WmRQhdCz5Nt35Y9lc1' \
 -H 'User-Agent: Recipes iPhone' -d '{ "name":"Labor Day Recipes" }' http://localhost:3000/cooks/1/cookbooks.json

Updating a resource

We also return no content when a resource is updated. We'll just return a status of 204 (no content). Any validation errors encountered during the update are handled like those when creating a resource.

curl -H 'Content-Type: application/json' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'X-Authentication-Key: TudE1T0Dx19wL82OAYTr22WmRQhdCz5Nt35Y9lc1' \ 
 -H 'User-Agent: Recipes iPhone' -X PUT -d '{ "name":"Memorial Day Recipes", "description": "My favorite recipes from Barbecue U" }' http://localhost:3000/cooks/1/cookbooks/1.json

Deleting a resource

Deleting a resource generates the same response as updating one.

curl -H 'Accept: application/json' -H 'User-Agent: Recipes iPhone' -H 'X-Authentication-Key: TudE1T0Dx19wL82OAYTr22WmRQhdCz5Nt35Y9lc1' -X DELETE \
 http://localhost:3000/cooks/1/cookbooks/1.json

A note about new and edit actions

One use for the new and edit actions in your API is to render a response body containing nodes that can be interpreted by the client and used to build an interface for creating and updating this resource. This way, the mobile app can handle changes in the API without requiging an update. Unfortunately, we can't rely on our mobile app having internet connectivity at all times.

Authentication and persistence

As we stated earlier, we're using Rails' native strategy for authentication and we're persisting user credentials using an auth_key attribute. After "session" has been established either by creating an account or by signing in, the auth_key value is stored in the persistent storage of the mobile device. We'll handle the sign-in functionality in the SessionsController class. Here, we'll only need a create action because the act of signing off (if even necessary) is done on the mobile app by removing the auth_key from the persistent store.

We need to start by adding a non-resource route for our controller action:

# config/routes.rb
match "/sessions.:format" => "sessions#create", :via => :get, :as => :session, :constraints => {:format => "json"}

Here's what our SessionsController looks like:

> see app/controllers/sessions_controller.rb
curl -H 'User-Agent: Recipes iPhone' -H 'Accept: application/json' -u username:password -G http://localhost:3000/sessions.json

We'll use simple HTTP Basic authentication using a Rails 3 helper method. We're not using a template for rendering since our response is just one node. Instead, we'll just use the render method and set the response content and headers within the action.

Clone this wiki locally