Perhaps other web frameworks come with similar powers, but of late I’ve been bowled over by Ruby on Rails’ routes. In Rails, routes are how a request like “
GET http://dx13.co.uk/blog/2008/01/07/Routing-in-Rails” is processed into a method call in the Rails application. It’s more powerful and flexible than anything I’ve worked with before in its domain.
Learning about routing in depth has taught me several methodologies and idioms used throughout rails, making it seem far less magical than it felt originally. Getting intimate with some portion of your chosen frameworks allows you to see into the minds of its creators in a way nothing else will, granting insights otherwise missed. This insight makes the workings of the framework more intuitive, meaning you can begin to work with the framework rather than merely in it.
I’ve not worked in many modern web frameworks. The previous version of this site was written in original ASP and I’ve dabbled in ASP.net over the past couple of years. These both seem to generally route requests based on what happens to be at the file system location pointed to by the URL’s path (that is, the part after
http://dx13.co.uk/). This seemed logical and I was happy with it; I didn’t imagine other ways of deciding what action should be performed for a given request.
More fool me.
Routes are a series of mappings rails uses to decide what to do with a given path string; routes are defined in the
config/routes.rb file of a rails site. Each mapping is a pattern which is matched against the path of the URL requested in the HTTP request. The request above,
/blog/2008/01/07/Routing-in-Rails, can showcase some of these powers at work.
Take the following route, which is defined in ruby code1:
The first line matches each portion of the URL, each one being separated by a “/[. Each portion starting with a]()” is similar to a variable, in that it stands in for whatever is in the real request. It can be seen that in our example:
- `:year` matches “2008”; - `:month` matches “01”; - `:day` matches “07”; - `:munged_title` matches “Routing-in-Rails”.
The “blog” in the pattern, which does not start with :, is matched literally against the word “blog” in the URL.
The second line of the route defines which controller and action will be called to generate a response to the request. This translates to a class and method call within the application.
The really nice thing about having the routing built into rails itself, as opposed to externally using a htaccess method, is the way the URL is passed on to the called action. Each
:variable in the request is passed to the calling method in the
params hash, which makes it simple to get the values used:
params[:year] returns “2008” and so on.
I found once I realised how the
params is filled from various parts of the request, the rails “magic” started to make much more sense. For example, the parameters in a POST request also make their way into the
params hash as sub-hashes, because they are also part of the HTTP request.
The third line states that
:munged_day can be omitted from the request URL and still be matched by the pattern. In this case we might want to allow a URL such as
/blog/2008 to show all posts for 2008, or
/blog/2008/01 to show all posts from January, 2008.
Allowing for portions of the URL to be missed out means a specific route doesn’t have to be defined for each possible option; we don’t need one for showing the posts from a given year, one for a given month and so on. Of course, the called action must be able to handle the cases when portions of the URL have been missed out.
Routing works hierarchically, meaning that you cannot miss portions from the middle of the request URL. You couldn’t use
/blog/2008/Routing-in-Rails for example. I find this adds a layer of safety because URLs can be assumed fairly sane if your routes are well crafted.
A problem we might encounter is the pattern as it stands will match any request with up to five portions starting with
/blog/, for example the obviously non-sensical
/blog/three/blind/mice. So we need to add something to make sure the URL is of the form we want.
:requirements argument in a route definition can be used for this.
:requirements allows for conditions to be placed on the format of any portion of the URL. They take regular expressions, so we can write an expression to make sure the year, month and day are valid before the route will be matched:
So ends half the story: how a request to a rails application is translated to a method call within the application.
The Routing powers of rails become even more interesting when you work out that
link_to and associated methods all link into this framework. They perform a kind of reverse match to see which route their arguments could be used to construct. An example would be:
would look through the routes for ones containing the variables mentioned in the method call, until it found our route above. It would work out that
:munged_title were not required, allowing it to generate
/blog/2008/01 as its result.
An interesting corollary of this is that, presuming you have used
url_for etc. consistently, URL patterns can be changed without breaking your application. Say one day you decide your musings require more gravitas in their addressing, you could rewrite the first line:
This would automatically cause your generated URLs to be more awe-inducing than before. One thing to note is that existing links to the site would be broken. This could be fixed by having two routes, one with “wisdom” and a second with “blog” which call the same action. Rails matches routes from top to bottom of the
routes.rb file, so placing “wisdom” above “blog” would ensure the generated URLs were impressive, whilst still supporting old links.
I find this end-to-end solution to be quite beautiful and a fascinating way to see into the mind of the creators of rails. It is Don’t Repeat Yourself at its finest, defining the URL structure of your application in one place and referencing that structure so naturally throughout all the stages of an application’s request-response lifecycle.
1 This example is taken from the routing section of the Rails Manual.