Keeping your routes RESTful

A while ago I blogged about RESTful routing in Rails. While Rails does help us in getting started with RESTful routing, it’s still very easy to lose your way and make a real mess of your routes.

To illustrate this, I’m going to recreate a problem I recently came across when working on RapidFTR – one of the social projects I’m involved in at ThoughtWorks.

Let’s see some code

To start off, I’m going to extend the example I was using in my last post about routing. I’m basically managing products – I’ve created a Product model, a Products controller and I have a single entry in my routes configuration.

resources :products

When I run rake routes I get the following list of routes.

List of routes

Right, now we have a new requirement – we need to be able to mark a product as a duplicate. We might have 2 admin users adding products and somehow we end up adding the same product twice. We don’t want to delete it (since some users might have bought the duplicate, etc) so we’re only going to mark it as a duplicate and link it to the non-duplicate product.

At the moment I can perform two actions on each product – view and edit. So I now want an additional action – mark as duplicate. This should take me to a page where I can select the non-duplicate version of the product.

Doing it the wrong way

Sounds easy enough – I’m probably going to need two additional actions on my controller – one to view the page and one to handle the page being submitted. To get started, let’s add 2 new routes.

get 'products/duplicate/:id' => 'products#new_duplicate', :as => 'new_duplicate'
post 'products/duplicate/:id' => 'products#create_duplicate'

Running rake routes reveals that we now have two additional routes.

Two Additional Routes

Now I just need to add two method to my Products controller. This could be done better (moving some of the functionality into the Product model, for example) – I put all the code into the controller to make it clear what’s going on.

def new_duplicate
  @product = Product.find(params[:id])
  @all_products = Product.where(['id <> ?', params[:id]])
end

def create_duplicate
  @product = Product.find(params[:id])
  @duplicate_of = Product.find(params[:duplicate_of_id])
  @product.duplicate = true
  @product.duplicate_of = @duplicate_of.id
  if @product.save
    redirect_to products_path
  else
    render 'new_duplicate'
  end
end

We can mark a product as duplicate! Hurrah! Are we done?

Doing it the right way

While this code certainly works, I don’t think it’s really the Rails way of doing it. It’s actually quite easy to end up with too many methods on your controllers – to avoid this, it helps to think in terms of resources, rather than models and controllers.

In this case, we can think of duplicates as being an additional resource. So instead of marking an existing product as a duplicate, we’re creating a new duplicate resource. As you’ll see, this enables us to use the regular REST verbs – new, create, edit, update, etc. Enough talking, let’s see how this would actually work!

I’m going to start off by changing the routes. Here I’m using a nested resource.

resources :products do
  resource :duplicate, :controller => 'duplicates', :only => [:new, :create]
end

Now our routes look a bit nicer.

Nicer routes

You’ll notice that our route now includes the :product_id, instead of just adding an id at the end of the route. This enforces the idea of a duplicate resource, which is available on product resources.

Now I only need to move my two actions to the newly created Duplicates controller.

class DuplicatesController < ApplicationController
  def new
    # same code as before
  end
  
  def create
    # same code as before
  end
end

More Examples

I first came across this concept in The Rails 3 Way – the common example seems to be in creating sessions. Instead of adding login and logout methods to the Users controller, we can think of sessions being a resource. So we might end up with a Session controller with new, create and destroy actions. Which actually makes a lot more sense.

If you would like to have a look at the code you can find it on Github. Happy coding.