The Exhaustive Guide to the Rails 3 Router

Rails 3 boasts a whole slew of improvements over the previous version, one of my favorite changes is the way the new router works. It’s a lot like the Merb router, but builds on the ideas and adds all sorts of functionality and configurability that is not possible (or is a lot more painful) in Rails 2 or Merb. One thing you can do to learn about the functionality of the router is open up the source files. They are in the ruby gem ActionPack in the action_dispatch folder. This is what I have done as well as looked at what others have done. The documentation is quite good in the source files.  However, for those who do not wish to locate and read the source and read multiple blog posts, I thought it’d be fun to go through and thoroughly document the behaviors and give a overview of how they work.

What Routes Do

A route takes a URL (like http://accentuate.me/photography/albums/) and breaks it up into variables that can be more easily used by your application.  Rails uses a few special variables to decide where to send the request, namely controller, action, and format.  The use of the variable id is a standard way of specifying to your application what record needs to be retrieved on RESTful routes, but this is not used by Rails directly. The nice thing is that you can also define your own variables to match different parts of the URL.

A Bit on REST

Perhaps the biggest hangup that a beginning Rails programmer would encounter in routing is the use of REST. REST stands for “Representational State Transfer”, a title which doesn’t do much to explain its purpose to the uninitiated.  If you have spent any time making forms which post information to a backend script, you should already be familiar with two verbs that are used in online communication: post and get. At first you might assume that the only difference between these two methods is the way in which they send variables. Get requests send variables in the URL whilst post requests send them in the request body – at least that is what I thought at first.

It turns out that these two methods (also called verbs) are actually indications of what the program on the web server is supposed to do. I will be using the word verb instead of method for the rest of the article. There are more than just the two verbs that web browsers support. There is also put and delete.

  • Get: This verb is used simply to retrieve information on a resource. Getting a web page or other response is all that should generally happen with a get request. Design limitations at times will preclude this, but as a general rule get requests should not modify anything.
  • Post: You use this verb to make a new resource. So any time you add a new record or create something, under RESTful design you should use the post verb.
  • Put: When you wish to update information on a resource that has already been created, you use the put verb.
  • Delete: This verb is used to delete or destroy information. An example would be deleting a row from a relational database.

There is a slight problem when using REST for routing: most web browsers only support the post and get verbs. As such, Rails uses a workaround to signal to your application what method to actually use. It adds a _method variable to all forms with the the desired verb when you use the form helpers. So when you use the following code:

<%= form_for '/posts/3', :method => :put do %>
<!-- your form code goes here -->
<% end %>

It will look something like this:

<form action="/posts/3" method="post">
<input name="_method" type="hidden" value="put" />
<!-- your form code goes here -->
</form>

The post verb is what matches the other unsupported verbs the closest in the way it is transmitted and so that is what is told to the browser whilst the real method is transmitted using a hidden form variable. If you are making requests via JavaScript you can make use of this _method variable as well.

Rails makes extensive use of RESTful principles in its router, and it is generally encouraged. You can also match URLs in other ways, however.

Show Your Routes

One of the most useful tricks to learn if you are just starting out with Rails is a command in your application: rake routes. In a terminal and in your rails app folder, type this command (rake routes) to get a listing of all the routes in your application. I use it on occasion when my routes aren’t working the way I think they ought.  It’s a bit hard to read at first, but is an indispensable tool. I can hardly wait until they add the route information to error pages like they do in the Merb framework, though.

The Route File

To get started in creating your routes, you need to edit the route file. This file is located at /your/app/folder/config/routes.rb. Of course, you will need to replace “/your/app/folder” with the location of your Rails application. The first thing you might notice on opening the file is that there’s not very much actual code. There are a lot of detailed comments that do a great job of getting you started, which will be covered here as well. You will want to put all code between the YourAppName::Application.routes.draw do |map| and the last end statements. Note that use of the map variable is not required anymore to match routes. It is included to assist those who prefer the older way of mapping routes. Your app will complain about the |map| variable being deprecated until you remove it. This backwards compatible feature will be eliminated in Rails 3.1 so it behooves you to learn the new way of routing.

URL Matching

URL matching is what lays at the heart of Rails routing. It enables you to specify a pattern that when matched allows you to take further actions. To make it easy, the method used to make a basic match is called match. There are different ways to define matching parameters based on different parameters and using other methods, but at the heart of each is the match method.

match ':controller(/:action(/:id(.:format)))'

This is an example that is generally not encouraged, especially if you are using a lot of resource based routes (which will be covered later). It does serve as a nice first example. You will take note of the variable names which are preceded by a colon. When a variable name is prefaced with colon, it means to match everything up until the next forward-slash. When the variable name is prefaced with an asterisk (*), it means to match all characters, which can be handy for passing full paths as a variable to your controllers.  Anything that is not prefaced with a colon or an asterisk is treated as a literal string. Anything that is surround by parentheses is optional. If it is present in the URL, the variable will be populated. Otherwise it will not be present or be nil.

match '/assets/*path' => "gridfs#serve"

Any path that includes /assets/ at the beginning will match this route. I use this route to serve files from a MongoDB server. A typical path is /assets/image/web/4c4f7be10190bd0f100001c0/thumb_9543.jpg, the string “image/web/4c4f7be10190bd0f100001c0/thumb_9543.jpg” is assigned to the variable path.

match '/*action' => 'pages'

This code allows me to define a controller and then just create erb pages in its views folder. If I create a file in Rails.root/views/pages/websites/pricing.html.erb for example and then go to the path /websites/pricing in my application, it’ll pull up the page without a fuss (after I’ve created the Pages controller via rails generate controller pages on the command line).

Modifying Match

You can add constraints to routes if you have needs of formatting the URL in a certain way or if you only want to match if a certain subdomain is present. If you only need to add a constraint to one match, use the :constraints option.

match ':controller(/:action(/:id))', :constrants => { :id => /\d+/ }

This code will ensure that any URL that matches this route must have a variable of ID that only consists of numbers. Since regular expressions are used, you have a lot of flexibility in how they are formatted. The only potential downside is that if you don’t know how to use regular expressions they can be difficult to understand. If you are serious about programming professionally, learn regular expressions and don’t whine about it. It’ll save your bacon more times than you will ever be able to keep track. (What’s the deal with programmers and bacon anyway?)

To add parameters to a bunch of match methods at once, you will want to use the scope method. For the sake of simplicity, we’re going to use the example above again.

scope(:constraints => { :id => /\d+/}) do
  match ':controller(/:action(/:id))'
end

You will note that the constraints option has been moved to the scope method, which is passed a block and the match method is called inside this block. If you simply wanted to define conditions and no other scope options (covered later), use the conditions method:

constraints :id => /\d+/ do
  match ':controller(/:action(/:id))'
end

This is a shortcut method that does exactly what the preceding example does.

Dispatching

Now that we’ve covered the very basics of matching, it’s time to talk about dispatching. Dispatching is calling a controller or even another application with the parameters that we’ve extracted from the URL using matching. If routing to an controller in your application, you have a few different options.

match "/people/:action', :controller => 'people' 

This routes to the people controller. Rails will read whatever is in the action variable and call that method in the controller instance. Like most other things, there is a shortcut for this as well:

match "/people/:action" => "people"
# or, if you want to match both controller and action
match "/people/laugh_at" => "people#laugh_at"

To provide a default variable value if none is supplied, first make that variable optional and then supply the value in the options.

match "/people(/:action)", :controller => 'people', :action => 'laugh_at'

Rails 3 has been reworked so that all controllers are basically Rack applications. Rack is a sort of framework for frameworks. You can use this knowledge to route to any sort of application or convention that also works with Rack.  Anything that responds to the method call can be a destination for your route. You can define a proc

match '/hello(/*path)', :to => proc { |env| [200, {}, "Hello, ugly!"] }

This will match any path after /hello/ to the proc. It is passed the raw environment variables (in env) that Rails gets from Rack, and returns a Rack compatible response ([http_status, http_headers, response_body]). If you don’t understand why, don’t try to overcomplicate it: Rack is designed to be ultra-simple and all that it requires from applications is that they return an array with those three variables.

Since all that is required is that the endpoint be Rack compatible, you can also include your Sinatra apps in your Rails routing setup if you have any.

match "/hello(/*path)", :to => YourSinatraApp

Using Verbs

Remember that little discussion we had about verbs before (post, get, put, delete)? Well, we’re going to use them now. Hopefully you’ve not forgotten about them. If you have then scroll back to the top and read it again. Don’t worry, I’ll still be waiting for you here when you’re finished.

To specify which verb is required when requesting a URL, use the :via option.  So to specify a post request to a controller/action on the root path (/):

match '/' => 'pages#send_request', :via => :post

Shortcut method

post '/' => 'pages#send_request'

The verb post can be substituted with get, put, and delete.

Resources

This is where all the interesting stuff with REST comes together. A resource is basically an idea of an object. So say you have a collection of images that you wish to display. In a very simple case, you would simply define this in your routes file:

resources :images

It doesn’t look like much, but essentially here’s what this code does:

get '/images' => 'images#index'           # list all images
get '/images/:id' => 'images#show'        # show one image
get '/images/new' => 'images#new'         # get form to create image
post '/images' => 'images#create'         # create image
get '/images/:id/edit' => 'images#edit'   # get form to edit image
put '/images/:id' => 'images#update'      # update image
delete '/images/:id' => 'images#destroy'  # delete image

As you can see (or might see after looking at it a few times), this sets up a whole lot of paths to list, show, create, update, and delete images. The only thing that you have to worry about is defining the methods in the Images controller that handle each of these methods – or at the very least defines the methods that you actually need.
There are also times that you want resources in resources. Blog posts have comments, and a logical choice for the URL for comments on a blog post is: /posts/3/comments. To achieve such a thing in the router:

resources :posts do
  resources :comments
end

When resource routes are constructed in such a way, the manner in which parameters are passed to the controller change as you go deeper in the hierarchy.  For the path “/posts/3″, the router will pass { :controller => ‘posts’, :action => ‘show’, :id => 3} to Rails and your application. For the path “/posts/3/comments” it will pass { :controller => ‘comments’, :action => ‘index’, :post_id => ’3′}. For the path “/posts/3/comments/25″ it will pass { :controller => ‘comments’, :action => ‘show’, :post_id => ’3′, :id => 25}.

Suppose you wanted to make resources for all of the replies to comments. This seems a bit convoluted to me, but this is just an example and not Real Life™.

resources :posts do
  resources :comments do
    resources :replies
  end
end

The path “/posts/3/comments/25/replies” would be routed to { :controller => ‘replies’, :action => ‘index’, :post_id => ’3′, :comment_id => ’25′}. Because it gets to be quite cumbersome, nesting resources below another nested resource is discouraged – but there are times when the complexity is justified.

Modifying Resource Behavior

To change which controller is used on a resource:

resources :posts, :controller => 'thingy'

This will now all route all of your posts URLs to the thingy controller, and anyone who inherits your code will hate you for life plus quite possibly hunt you down to beat you senseless. Please name and route your controllers in a somewhat obvious way for all of humanity’s sake.

To do custom matching on URLs in resources, there’s an extra step that you have to take. With resources there are two groups of matchers: collection and member. So with our posts resource, the index action would be considered part of the collection group, and the edit action part of the member group. A simple way to remember is if it requires the :id parameter, it’s a member function. Otherwise, it’s a group function.


# This works well for single custom actions
collection :posts do
  get :unpublished, :on => collection
  # For when you want to show a HTML delete page
  # instead of confirming via Javascript
  get :delete, :on => :member
end

# This works better for multiple actions
collection :posts do
  collection do
    get :unpublished
    # Contrived. Maybe you want a method to set an
    # arbitrary order?
    put :reorder
  end
  member do
    get :delete
  end
end

So the additional URLs will be matched: “/posts/unpublished” (get), “/posts/reorder”, (put), and “/posts/3/delete” (get)

URL Generators

Part of the router is taking URLs apart into variables that can be used by Rails and your application, but the other part is doing the reverse: taking variables and making them into a URL to send back to the requesting client. When you use the resources method, the names are generated automatically. When using the match method, you have to define the names yourself if you wish to use URL generators on that route.

match '/posts/:id' => 'posts#show', :as => 'post'

This will generate two methods in all of your controllers named post_path and post_url. As you might infer, the _path suffix just returns the URL path whilst the _url suffix returns a full URL with hostname and port. You can call post_path(post_object) and Rails will attempt to infer the ID from the object that was passed. If you’d prefer to set the parameters yourself you could also call post_path(:id => post_instance_object.id) to generate the URL. Anything extra passed in like post_path(post_object, :msg => "Hello") will generate this URL: “/posts/3?msg=Hello”

Using the post resources code we used in the Resources section, to get the URL for a specific comment you would call post_comment_path(post_object, comment_object) or post_comment_path(:post_id => post_object.id, :id => comment_object.id). if you don’t pass the required variables to a URL helper method, it will attempt to grab the variables from the parameters on the current route.

Personally I don’t like the way Rails clutters up the space with a bunch of helper methods, I prefer the Merb way of using two methods to access routes, though I would like different names: path(:post_comment, post_object, comment_object) and url(:post_comment, post_object, comment_object). Maybe if I whine about it enough they’ll put it in Rails 3.1 :).

Constraining and Scoping Fun

There are all sorts of ways in which you can use the constraints option. Here are a list of known ones

  • ip You can specify a match on IP address. :ip => /127.0.0.1/
  • subdomain www.accentuate.me will match :subdomain => /www/
  • There are a ton more. I will be updating this article shortly with many more options once I test them out or people suggest them in the comments.

There are a few other options you can use besides constraints as well.

  • path Prepend a path to the contained matchers
  • module Useful for when your controllers are in modules such as Blog::PostsController
  • name_path Prepend a string to the name of the helper methods.
scope(:path => 'blog', :module => 'blog', :name_path => 'blog') do
  resources :posts
end

This will match ‘/blog/posts’ to { :controller => 'blog/posts', :action => 'index'} and will also create the helper of blog_posts_path for use in your controllers in constructing the URL. A shortcut method for all that code is namespace

namespace 'blog' do
  resources :posts
end

Redirection

If you need to send a request to a different URL, the redirect method is here to save the day! The redirect method takes either a string argument with the URL or a block that accepts the parameters generated from the route and optionally the request object.

match '/old/path' => redirect('/new/path')
match '/clunkypages/:page' => redirect('/shinypages/%{name}')
match '/clunkypages/:page' => redirect { |params| "/shinypages/#{params[:name]}"
match '/clunkypages/:page' => redirect { |params, request|
  if request.referer.match('news.ycombinator.com')
    # Send that thing back where it came from! >:)
    "http://news.ycombinator.com"
  else
    "/shinypages/#{params[:name]}"
  end
}

As you might have been able to infer, the redirect method has a handy shortcut method for dealing with parameters. Use the %{param_name} shortcut to simplify and avoid using a block if all you need to get is certain parameters. The string that is the new URL needs to be the last executed line of the block. This is what is returned to the redirect method.

If you care about 301 vs 302 statuses: thank you for caring. The redirect method uses the search engine friendly 301.

Conclusion

90% of the time, you won’t need most of what you read here – but the beauty is that when you do have a special routing case that the new Rails router is flexible enough to meet just about any need.  Those of us who have crazy ideas about hooking up multiple applications in one ruby process or want to store route parameters in a database can now do so much more easily in Rails 3. Happy hacking!

References

Rails Dispatch: The Powerful New Rails Router

Yehuda Katz: The Rails 3 Router: Rack it Up

Wikipedia: Representational State Transfer

Ruby on Rails Guides: Routing

This entry was posted in Programming, Rails, Ruby. Bookmark the permalink.

8 Responses to The Exhaustive Guide to the Rails 3 Router

  1. brs says:

    Typo in your first example under “Modifying Match”; ‘constrants’ for ‘constraints’.

    (A bit odd to comment 1+ years after you made the post, but I’m upgrading my companies routes from 2.3.x syntax to 3.0.x syntax now, hence looking at all sorts of routing resources. Good post on the whole!)

  2. RyanonRails says:

    Great post on routes!

    I didn’t realize you could limit routes via ip and subdomain. That’s pretty cool.

    -Ry

  3. NEX-5N says:

    I envy your capability to publish wonderful article – simply wanted to say I like this !

  4. Dave Bone says:

    Well written. Keep up the public comments. As i’m just getting into Rails this has highlighted the implicitness of Rails actions.
    thk u Dave

  5. jamesw says:

    Fantastic write up. thank you for sharing.
    I was wondering if it is possible to add a constraint to a number of routes that would redirect to a different host?

  6. Cameron Carroll says:

    Just want to thank you, as I’ve been wading through docs on Rails routing for a few hours now trying to solve my problem. Your example on route globbing is what saved the day.

    Cheers!

    P.S. — Almost didn’t even look at this website because of the fonts used. Glad I persevered. No offense. <3

  7. Iain says:

    Great article.

    It wipes the floor with the Rails Guide to routing for readability.

  8. kingCrawler says:

    great article. thanks for the info it was very helpful to me.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>