What's New in Edge Rails: Cleaner RESTful Controllers w/ respond_with

Posted by ryan
at 9:52 PM on Wednesday, August 05, 2009

This feature is schedule for: Rails v3.0

REST is a first-class citizen in the Rails world, though most of the hard work is done at the routing level. The controller stack has some niceties revolving around mime type handling with the respond_to facility but, to date, there’s not been a lot built into actionpack to handle the serving of resources. The addition of respond_with (and this follow-up) takes one step towards more robust RESTful support with an easy way to specify how resources are delivered. Here’s how it works:

Basic Usage

In your controller you can specify what resource formats are supported with the class method respond_*to*. Then, within your individual actions, you tell the controller the resource or resources to be delivered using respond_*with*:

1
2
3
4
5
6
7
8
9
10
11
12
13
class UsersController < ApplicationController::Base

  respond_to :html, :xml, :json

  def index
    respond_with(@users = User.all)
  end

  def create
    @user = User.create(params[:user])
    respond_with(@user, :location => users_url)
  end
end

This will match each supported format with an appropriate response. For instance, if the request is for /users.xml then the controller will look for a /users/index.xml.erb view template to render. If such a view template doesn’t exist then it tries to directly render the resource in the :xml format by invoking to_xml (if it exists). Lastly, if respond_with was invoked with a :location option the request will be redirected to that location (as in the case of the create action in the above example).

So here’s the equivalent implementation without the use of respond_with (assuming no index view templates):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UsersController < ApplicationController::Base

  def index
    @users = User.all
    respond_to do |format|
      format.html
      format.xml { render :xml => @users }
      format.json { render :json => @users }
    end
  end

  def create
    @user = User.create(params[:user])
    respond_to do |format|
      format.html { redirect_to users_url }
      format.xml { render :xml => @user }
      format.json { render :json => @user }
    end
  end
    
end

You can see how much boilerplate response handling is now handled for you especially if it’s multiplied over the other default actions. You can pass in :status and :head options to respond_with as well if you need to send these headers back on resources rendered directly (i.e. via to_xml):

1
2
3
4
5
6
7
8
class UsersController < ApplicationController::Base

  respond_to :html, :xml, :json

  def index
    respond_with(@users = User.all, :status => :ok)
  end
end

Per-Action Overriding

It’s also possible to override standard resource handling by passing in a block to respond_with specifying which formats to override for that action:

1
2
3
4
5
6
7
8
9
10
11
12
13
class UsersController < ApplicationController::Base

  respond_to :html, :xml, :json

  # Override html format since we want to redirect to a different page,
  # not just serve back the new resource
  def create
    @user = User.create(params[:user])
    respond_with(@user) do |format|
      format.html { redirect_to users_path }
    end
  end
end

:except And :only Options

You can also pass in :except and :only options to only support formats for specific actions (as you do with before_filter):

1
2
3
4
5
class UsersController < ApplicationController::Base
  respond_to :html, :only => :index
  respond_to :xml, :json, :except => :show
  ...
end

The :any Format

If you’re still want to use respond_to within your individual actions this update has also bundled the :any resource format that can be used as a wildcard match against any unspecified formats:

1
2
3
4
5
6
7
8
9
10
11
12
class UsersController < ApplicationController::Base

  def index

    @users = User.all

    respond_to do |format|
      format.html
      format.any(:xml, :json) { render request.format.to_sym => @users }
    end
  end
end

So all in all this is a small, but meaningful, step towards robust controller-level REST support. I should point out that the contributor of this patch is José Valim who has authored the very robust inherited_resources framework that already has support for respond_with-like functionality and many more goodies. If you’re on the search for a solid RESTful controller framework to accompany Rails’ native RESTful routing support I would suggest you take a look at his fine work.

tags: ruby, rubyonrails

Comments

Leave a response

  1. José ValimAugust 06, 2009 @ 03:04 AM

    Indeed this is “a small, but meaningful, step towards robust controller-level REST support”. I’m working with Jeremy and Yehuda to extend it even more.

    Could you please just update the link to my blog to http://blog.plataformatec.com.br/ ? The one in the post is deprecated. ;)

    Thank you for the excellent coverage of the features added, as always!

  2. Carlo PecchiaAugust 06, 2009 @ 04:09 AM

    Really nice feature introduction: small but effective!

  3. Glenn GillenAugust 06, 2009 @ 06:19 AM

    Looks like a great addition, good work guys.

    Any chance there is something in the pipeline to address the default behavior of the to_* serialization on models? In particular, point #2 in “Where’s the problem?” on this page:

    http://playtype.net/past/2008/5/20/rest_on_rails_filling_in/

    Glenn

  4. Tom WardAugust 06, 2009 @ 06:45 AM

    This is one of those tiny changes that could make a big difference. I’m more excited by these steps towards maturity than headline-grabbing new features. Thanks to José

  5. Ryan DaigleAugust 06, 2009 @ 07:12 AM

    José, I changed the link to your blog. You may want to update your github profile as well, as that’s where I pulled the link from. Thanks again for this patch – and looking forward to seeing how the rest falls into place!

  6. Mark RichmanAugust 06, 2009 @ 07:16 AM

    Any way to add your own formats, i.e. PDF?

    I’d love to be able to do index.pdf.erb or index.pdf.haml!

  7. José ValimAugust 06, 2009 @ 08:43 AM

    @glenn I would love to see that feature too, since Josh Peek is refactoring ActiveModel we could convince him to do that! ;)

    @ryan Thanks! The blog was released this week I’m still updating all links. Just done that with Github ones. Thanks again!

    @mark I’m quite sure that this is a already possible using Mime::Type.register.

  8. Amit KumarAugust 06, 2009 @ 08:30 PM

    Nice !!!! This piece in our controller was not DRY…. small but effective push !!!

  9. José ValimAugust 07, 2009 @ 02:57 PM

    Ryan,

    I just pushed a commit with documentation about nested resources. The syntax is the same as in polymorphic_url and form_for:

    def create
      @project = Project.find(params[:project_id])
      @task = @project.tasks.build
      @task.save
      respond_with([@project, @task])
    end

    This is important because it will tell respond_with to redirect to the proper url. Since a lot of people use yours posts are reference (including me), it looks like a good addition. :)

    Regards!

  10. Travis DunnAugust 10, 2009 @ 08:44 AM

    Wow, this is great; v3.0 can’t arrive soon enough. The respond_to blocks drive one crazy after awhile (assuming one is the kind of developer who is anal about keeping their classes DRY, let alone code just a line or two apart).