simplier restful

Jared Carroll

One of the biggest features in the latest version of Rails is RESTful routes.

By writing the following in your routes.rb file (assuming you have a UsersController):

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

You get the following named routes for free (excluding named routes that include a :format extension):

GET    /users         #index
POST   /users         #create
GET    /users/new     #new
GET    /users/1       #show
PUT    /users/1       #update
GET    /users/1;edit  #edit
DELETE /users/1       #destroy

The first column is the HTTP verb. Now Rails knows that browsers don’t support the HTTP verbs PUT and DELETE, so it fakes it by doing POSTs instead, and passing along a hidden field parameter named _method whose value is either ‘put’ or ‘delete’.

So in order to support the basic CRUD actions in a controller, you need 4 unique routes. To me, the routes for #new and #edit are ugly. If we just had more HTTP verbs, we could eliminate both of those routes and reduce the total number of unique routes to 2 (actually, we could go all the way to just 1 route). Since Rails just uses POSTs and hidden parameters for PUT and DELETE, we can easily add our own HTTP verbs.

However, we can’t use #resources anymore in our routes.rb file though, instead we’ll have to create named routes for each of our 7 actions.

ActionController::Routing::Routes.draw do |map|
  # Create
  map.new_user 'users',
  :conditions => { :method => :new },
  :controller => 'users',
  :action => 'new'

  map.users 'users',
  :conditions => { :method => :post },
  :controller => 'users',
  :action => 'create'

  # Read
  map.users 'users',
  :conditions => { :method => :get },
  :controller => 'users'

  map.user 'users/:id',
  :conditions => { :method => :get },
  :controller => 'users',
  :action => 'show'

  # Update
  map.edit_user 'users/:id',
  :conditions => { :method => :edit },
  :controller => 'users',
  :action => 'edit'

  map.user 'users/:id',
  :conditions => { :method => :put },
  :controller => 'users',
  :action => 'update'

  # Delete
  map.user 'users/:id',
  :conditions => { :method => :delete },
  :controller => 'users',
  :action => 'destroy'
end

The above code added 2 new HTTP verbs via the new :conditions parameter:

  1. new
  2. edit

Those will handle the form display for creating a new user and editing an existing user. Now our app only needs 2 unique routes:

  1. users
  2. users/:id

We can use the normal #link_to to create links to our #new and #edit actions by using the :method parameter:

link_to 'new', user_url(@user), :method => :new
link_to 'edit', user_url(@user), :method => :edit

This really is going to create a handler for this link’s onclick event that creates a form to the specified URL and POSTs to it with a hidden parameter value for _method of either ‘new’ or ‘edit’ (i.e. this won’t degrade gracefully with javascript disabled, if you want that you’ll need to create forms that contain nothing but a button - unfortunately #button_to doesn’t support the :method parameter).

Of course, the major downside of all this is that users can’t bookmark links to the #new and #edit pages. But I like the simplicity of only having 2 routes. You could take this all the way down to 1 route, but I think 2 is better:

  • one for the collection of users
  • one for an individual user