Dynamic Routes in Rails Redux

A little over 3 years ago - not long into my adventures with Rails - I posted an article titled Dynamic Routes in Ruby on Rails. This article has been bugging me for a while lately for one big reason - it's one of the most popular articles on my blog, and it's woefully outdated and a terrible guide to just what you can do with Rails' routes.

So, this post is going to look at what's changed since then, and how I would solve the same problem today. I'm also going to update a few of the other points I made in that post - in particular updating my somewhat unjustified stance on using method calls to produce markup.

First off, Rails' routing system is a lot smarter than it was 3 years ago. It now easily handles non-ID "human" URLs with much less hassle than I went through, and supports many other powerful features that we would never have expected back then.

In the case of my original post, I wanted a URL structure like :

 /documents-and-services/agreements

to be resolved to a request such as :

 { :controller => 'categories', :action => 'list', :name => 'agreements' }

In the old example, I put the responsibility on the application load-up by making routes.rb load up all my categories and generate the routes. Now, this might work OK in development mode, or in an application that doesn't change often. However, in a production application, any time that you wanted to add a category you would have needed to restart the application to "refresh" the routes. Not a good solution.

So how would we do this now? Well, there's a few ways.

Firstly, we can do the same thing as above much simpler using the routing system's support for regular expressions. Effectively, we would put the responsibility on the action to find the category that we want to display - it's going to need to find the row anyway, so this isn't any different to a normal Rails action. In this case, we would rewrite our routing rule as :

  map.connect 'documents-and-services/:name', :controller => 'categories', :action => 'list', :name => /[A-Za-z0-9\-_]/

This rule tells the routing system that the :name parameter can match the given regular expression (any letter, number, a dash or underscore). The controller/action parameters are exactly like any typical routing rule, and say that it should route through to that particular method.

In the action, we would then find our category, exactly the same as normal (but using the :name parameter).

  @category=Category.find_by_name(params[:name])

Problem solved without any nasty code in the routes.rb file.

An alternative solution, and one which I'm particularly fond of, is setting a custom ID parameter method that will be used in the URL. I find this is often useful in places where it wouldn't be feasible to have a 1-to-1 relationship between the data in the URL and your rows.

For example, let's assume that the document system we were talking about above allows multiple categories with the same name - perhaps differentiated only by the link the visitor clicked. In these cases, a :name parameter would be insufficient to find the single category to display.

Another instance where the non-ID parameter can become a problem is when the data you need to display can not easily be indexed, and therefore will cause a slow database request while it finds the matching row. Although this is uncommon, it does sometimes happen.

In these cases, I like to change the model's to_param method in order to change the id parameter produced by url_for(), and therefore link_to().

A brief rundown - when generating an :id for a URL, url_for calls the to_param() method of the object provided. Unless you override this, it's provided by ActiveRecord::Base to return just the integer ID of the row. However, to_param gives us a heap of flexibility.

My usual trick is to change to_param to something like this :

  def to_param
    "#{self.id}-#{self.name_for_url}" 
  end
  def name_for_url
    self.name.gsub(/^A-Z0-9\-_/i,'')
  end

The above code is fairly self explanatory - we're overriding the to_param() method to output the id and an encoded version of the model's name. We need to encode it, as we don't want "bad" characters like apostrophes, punctuation marks or other special characters messing up the URL.

Given a routing rule like :

  map.connect 'documents-and-services/:id', :controller => 'categories', :action => 'list', :id => /[0-9]+-[A-Za-z0-9\-_]/

link_to will generate nice looking URLs like :

  /documents-and-services/14-agreements

The important thing to note here is that when reading the details back in, you don't have to worry about the string portion of the value. When the string is coerced to an integer, the "-agreements" is simply ignored, leaving the ID value as just 14. In this way, the name after the ID is purely for vanity purposes, and can easily be changed and rejigged without any risk of the data changing. It's also a lot more robust, as changing the name of the model doesn't instantly kill all of your links.

Incidentally, this is the way that we currently neaten the URLs on the recently re-launched World Stock Exchange site. The Company model has a to_param similar to the above and this allows us to show nice URLs which actually show the name of the company, rather than just a plain ID.

One final thing I wanted to address from the original post concerns my former dislike for "writing HTML as method calls". Part of this was angst from all of the shitty frameworks I've worked with in the past, and certainly with the crufyness of some of the Rails helpers 3 years ago. Now, however, it's a whole different ball game.

A clever combination of routing and link_to ensures that you can easily make wide scale "neating" or structural changes to your URL scheme with trivial ease - change the routes.rb configuration and all of your links magically change to match. Not to mention that named routes can dramatically neaten up your views. What's nicer?

  link_to('Your Profile', :controller => 'users', :action => 'show', :id => @user)
  or
  link_to('Your Profile', profile_url(@user))

Similarly, start_form_tag and end_form_tag once left nasty tastes in my mouth. Now, however, I'm deeply in love with the form_tag block - not only does it include XSRF protection out of the box in a nice, block based method, but it is logically neater than disconnected method calls for what is effectively an element wrapping content.

I think the sensible use of method calls to generate code (eg. helpers) makes writing content much easier, and much more maintainable. As for CGI.pm's start_html/end_html functions that I bitched about in my original post? Yup - I still hate them. It's messy. Use a template system - whether it's something like erb or HAML, or one of the huge number of other template systems.

There's a lot of things that I've learnt in the past 3 years since I wrote that post, and there's even more that's changed since then. We've seen Rails make a 1.0 release, a 2.0 release, and development is still continuing.

So… I'll probably have to come back and revise this post in 3 years…