Basic Routing in Rails

Posted by Mike
Liquid error: wrong number of arguments (5 for 2)

Coming (as I did) from an ASP.NET background, the support for routing in Rails was one of many aspects of this new world that impressed me considerably. It’s easy to see why that should be so: the support for a similar facility in ASP.NET (mapping of friendly URLs to internal URLs) is rudimentary at best. The reasons why point up the difference in approach between the two development teams: Microsoft has apparently chosen to avoid shipping any usable URL rewriting code until they can bombproof it for every conceivable use and size of enterprise, while the Rails team has chosen to give us something that’s good enough for most applications right now.

The point of routing is quite simple: it decouples the internal design of your Rails application from its external interface. For example, it may make perfect sense to you to have all of the methods that deal with handling image management, including the administrative tools for uploading images and a method for showing images, within a controller named upload. But that doesn’t mean that referring to an image within your application must be done with a URL such as /upload/show/4. With routing, you are perfectly free to use a URL such as /images/4 instead.

To do this, you define a route within the file config/routes.rb in your application. In this particular case, the route would look like this:


# show a single image
 map.connect '/images/:id', :controller => 'upload', :action => 'show'
 

You can think of the routes.rb file as a set of rules that is followed by Rails in dispatching incoming HTTP requests to the controllers that will handle them. Rails scans down these rules in order and sends each request to the first matching rule. In this particular case, the rule matches any incoming request for the exact text /images/ followed by precisely one word (in the regular expression sense of a contiguous set of letters and numbers). The word becomes the :id parameter which is passed to the show action within the upload controller. So this particular route would be invoked for /images/16 but not for /images/16/172.

As it stands, this route would also be invoked for /images/wintergreen.jpg, which is a problem if your show action is expecting to be passed an :id parameter. Generally it’s considered poor form for your application to blow up in the user’s face. To avoid having routes try to process requests that they can’t handle, you can qualify them further with requirements expressed as regular expressions. In this particular case, we can add the requirement that the :id parameter be numeric:


map.connect ’/images/:id’, :controller => ‘upload’, :action => ‘show’, 
                           :requirements =>{ :id => /\d+/ }

This can be expressed more concisely as:


# show a single image
 map.connect ’/images/:id’, :controller => ‘upload’, :action => ‘show’, :id => /\d+/

But hold on a minute here: surely in a well-designed application, you’d like to allow people to retrieve images by name as well as by id. That’s certainly possible; all that you need to do is define another method (let’s call it find_by_name) in the controller and add another route. In this case, though, you must include requirements in the route. Why? Because by default, the dot character, like the forward slash, is treated as a separator between elements in a route. So you need to explicitly tell Rails that you want the dot treated as part of a parameter:


# show a single image by name
map.connect ’/images/:imagename’, :controller => ‘upload’, :action => ‘find_by_name’, 
                                              :imagename => /.*/   

Another potential use of routes lies in protecting some URLs from being inadvertently called in a way that can lead to unwanted side-effects. You can use conditions to route requests differently depending on which HTTP verb is used to invoke them. For example, consider this pair of routes:


map.connect 'admin/update/:id', :conditions => { :method => :get }, 
                                :controller => "admin", :action => "list" 
map.connect 'admin/update/:id', :conditions => { :method => :post }, 
                                :controller => "admin", :action => "update" 

If a URL such as /admin/update/7 is called via an HTTP GET (perhaps as a result of an overenthusiastic search-engine listing), the first route will redirect this to the list action. Only requests that come in via POST (presumably from an editing form) will be processed by the second route and sent to the update action.

Routes can be made even more flexible with the addition of defaults. Supplying defaults allows you to create routes where trailing portions of the URL can be omitted. Going back to the image handling example, suppose you wanted to show an image catalog when the plain /images URL is accessed? Here’s a route to handle that:


map.connect 'images/:action/:id', :defaults => { :action => "catalog"}, 
                                  :controller => "upload" 

If you don’t supply an :action parameter, the value catalog will be used. There are two special defaults – “default defaults,” if you will. By convention, :action defaults to index and :id defaults to nil. This explains how a single default route can handle the dispatching chores in a simple Rails application:


map.connect ':controller/:action/:id'

Finally (for now), the last element of a route path can be a catch-all element, using the syntax *name. Here’s a route that catches anything, no matter how strangely-composed:


map.connect '*incoming' :controller => "admin", :action => "sitemap" 

The incoming parameter in this case will point to an array containing all of the components of the incoming URL. Because this route will match anything, it must be the last one in your routes.rb file (well, there can be other routes after it, but they will never match anything because Rails processes routes from the top of the file down).


That’s enough to get started with Rails routing for the purpose of URL rewriting, but there’s a lot more to learn. In particular, I haven’t covered the creation and use of named routes, or the use of the new resource-based routing. I may get to them in future installments.

Comments

Leave a response

  1. Lame ProgrammerFebruary 09, 2007 @ 12:04 PM

    Apache mod_rewrite already does this, for years and years? I’m not sure how having rails do this is an improvement over letting the webserver handle it.

  2. Mike GunderloyFebruary 09, 2007 @ 12:21 PM

    Three things. First, Rails isn’t tied to Apache; this will work whether the server you have handing out results has mod_rewrite support or not. Second (although I am not an Apache expert) I am fairly sure that Rails routing is more flexible than mod_rewrite. Perhaps after I write the rest of the story someone can tell me whether that’s true.

    Third, on a more philosophical level, I’d rather have the routing details in a file that’s part of my Web application than in the Apache configuration file. Limiting the coupling between the application internals and the Web server setup seems like a good thing to me.

  3. Lame ProgrammerFebruary 09, 2007 @ 12:58 PM

    Wow…can’t read tone I wouldn’t bet your pay check on mod_rewrite be less flexible then rails routing.

    Although I agree with your first point, in production systems Apache and IIS are the only real options.

    On your third one I don’t know where the lines are drawn and I’m certainly not going to argue the point so lets call you the winner 2 out of three. Rails routes are declared better than sliced bread!

  4. Mike GunderloyFebruary 09, 2007 @ 01:29 PM

    No tone meant – mod_rewrite is on the (long, long) list of things I need to learn more about as I make the transition from one stack of tools to another. If I get to the point where I can make a definitive comparison I’ll let you know :)