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…