Where do you REST your head?

by Sean Cribbs

Sure, it's nearly effortless to add a new XML-based API to your application using the latest stuff in Rails Edge, but what do you do when the Sandman comes? (Sorry about all the puns!)

Lately, I've been very interested in the stuff coming out in Rails Edge and eventually 1.2, emphasizing the CRUD design technique and using REST based services. A lot of this has been spurred on by DHH's keynote at RailsConf that I watched via the web.

The technique is liberating! All your controllers do basically the same things, which are the major CRUD operations (index/show, create, update, destroy) and the typical special representations (edit, new). However, when it comes down to the nitty gritty, there are a few concerns I still have.

Just to test my understanding of the domain and technique, I set out prototyping a new application this morning. It's based on an old PHP app we have that is used to manage the supplemental online databases that KCKCC Library owns or subscribes to. The basic format is very simple: Databases belong to a Category and each have several simple fields, with a few validations.

I started with the CategoriesController to see if I could figure everything out. I created the seven basic actions and added the default respond_to blocks. Here's a simple example:

  def index
    @categories = Category.find(:all)
    respond_to do |format|
      format.html
      format.js
      format.xml { render :xml => @categories.to_xml }
    end
  end

That one was pretty straightforward. What became a brain-twister were the methods that actually modified something. Ideally, in an XML API you don't have to print out a big response when a record was created, deleted or updated. The way you do that is with status codes, and it is something that showed up in DHH's talk. Here's how I would do the create action:

  def create
    @category = Category.create(params[:category])
    respond_to do |format|
      format.html { redirect_to :action => :index }
      format.js
      format.xml do
          headers["Location"] = category_url(@category)
          render :nothing => true, :status => 201
      end
    end
  end

Simple, right? You just send back the location of the new record in the header, and return a 201 status code (Created). This is all well and good until you have some misbehaving clients. What if the new record wasn't created because it failed validation? With our HTML templates, we might do this:

      format.html do
        if @category.valid?
          redirect_to category_url(@category)
        else
          render :action => :new
        end
      end

But what should we do with the XML API? We need to refer to the HTTP 1.1 status codes, of course, specifically the 4xx ones, which refer to errors on the client-side. Here are our options (from here):

400 Bad Request
This is primarily for malformed requests. Personally, I think this is inappropriate because it doesn't say that the record submitted failed validations, but that it had bad syntax -- not exactly the same thing.
406 Not Acceptable
At first, I thought this would be it, but actually it refers specifically to the Accept: header and that the resource cannot be returned in an acceptable format.
409 Conflict
Finally, a good candidate! Quoth W3C, "The request could not be completed due to a conflict with the current state of the resource. This code is only allowed in situations where it is expected that the user might be able to resolve the conflict and resubmit the request." They go on to talk about potential uses for versioned resources, etc. Most importantly, however, they mention that the response should give adequate information about how to resolve the conflict.
412 Precondition failed, 417 Expectation failed
These both looked good at first, but of course they refer to specific HTTP headers just like 406 does. Sorry, boys, you're out.

So in the end, I decided 409 was the best for the create action. Now, we need to provide information about "how to resolve the conflict". Why not return the errors that were added to the object?

      format.xml do
        if @category.valid?
          headers["Location"] = category_url(@category)
          render :nothing => true, :status => 201
        else
          render :xml => @category.errors.to_xml, :status => 409
        end
      end

Obviously, there are other errors to deal with, but for the create and update actions, 409 will work just fine.

I'd love to hear your thoughts on how you tackled this issue, or what you would use for status codes in certain situations.

© 2006-present Sean CribbsGithub PagesTufte CSS