Custom REST routes

Posted by matt on July 17, 2007

When developing rails applications using REST, all controllers have only seven actions (index, show, new, create, edit, update and destroy). Occasionally there is a need for additional custom actions. They can be added very simply in config/routes.rb. For example, in part of an application showing a list of venues, you may want to list only venues which have not closed down, and you may want to mark a venue as closed_down.

Your initial config/routes.rb setup would look something like this:

  ActionController::Routing::Routes.draw do |map|
    map.resources :venues
  end

To list all venues that are closed down you would first need to add the following action to your venue_controller.rb:

  def closed
    @venues = Venue.find(:all, :conditions => ['closed_down = ?', true])
  end

However, trying to navigate to this action will result in a routing error. To fix this, you will need to amend routes.rb:

  ActionController::Routing::Routes.draw do |map|
    map.resources :venues, :collection => {:closed => :get}
  end

The collection hash is used to specify actions which reference collections of objects instead of actions which reference one object. In this case the closed action is referencing a collection of venues. Inside the hash :closed is mapped to a GET request. This will also automatically create a named route: closed_venues_path. Cool!

So that’s working. Now to be able to mark a venue as closed_down.
In your venues_controller.rb you would add the following action:

def mark_closed_down
  @venue = Venue.find(params[:id])
  @venue.update_attribute :closed_down, true
  redirect_to closed_venues_path
end

However, trying to navigate to this action will currently throw an error, as following the current RESTful setup the application will try to render the show action, which will try to find a venue with id => ‘mark_closed_down’.

To fix this you need to make the following change to your routes.rb file:

  ActionController::Routing::Routes.draw do |map|
    map.resources :venues, :collection => {:closed => :get}, :member => { :mark_closed_down => :put }
  end

The member hash is used to specify actions that reference one object in particular. In this case mark_closed_down is referencing one venue. Inside the hash :mark_closed_down is mapped to a PUT request. This also gives you a named route: mark_closed_down_venue_path(venue). Thats it!

You can add as many custom routes as you like to each controller, but you should do so with caution. If you find yourself needing lots of them it probably means that you have missed a model, or that you should create another controller. Each controller maps one to one with a resource, but not necessarily with a model. In this case, for example, it would be possible to map another resource in routes.rb maybe called closed_down_venues:

map.resources :closed_down_venues

Then create a closed_down_venues controller with:

script/generate controller closed_down_venues

The index action would then be the list of closed venues, and the update action would be the mark_closed_down action.

I hope this is all clear. Please add a comment if not, and I will do my best to clarify…

Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. Rick DeNatale Tue, 17 Jul 2007 12:37:57 BST

    The use of a get request to mark a venue as closed down doesn’t seem very RESTful to me.

    Shouldn’t this be done with an update request rather than a get?

  2. matt Tue, 17 Jul 2007 14:55:55 BST

    You are absolutely right Rick. I really should proof read my posts. I have updated that to a PUT request.

  3. Frank Falkenberg Fri, 20 Jul 2007 13:41:24 BST

    I would recommend to delegate the closed and mark_closed_down methods to the Venue model and leave the controller as dumb as possible. That makes it much simpler to refactor the controllers.

  4. Joël AZÉMAR Thu, 26 Jul 2007 15:51:42 BST

    Thanks for this article, that helped me much !

    and if you want a different form for this same new object, you can customize route like :

    map.resources :venues, :new => { :good => :get } or
    map.resources :venues, :new => { :bad => :get }

    This will create a named route:
    good_new_venue_path and bad_new_venue_path ;-)

    look Module ActionController::Resources API for more comprehension

  5. matt Mon, 06 Aug 2007 21:38:31 BST

    I’m glad to have helped you Joel.

    Thanks for the workingwithrails recommendation!

Comments