Yehuda Katz is a member of the Ruby on Rails core team, and lead developer of the Merb project. He is a member of the jQuery Core Team, and a core contributor to DataMapper. He contributes to many open source projects, like Rubinius and Johnson, and works on some he created himself, like Thor.
The Rails 3 Router: Rack it Up
December 26th, 2009
In my previous post about generic actions in Rails 3, I made reference to significant improvements in the router. Some of those have been covered on other blogs, but the full scope of the improvements hasn’t yet been covered.
In this post, I’ll cover a number of the larger design decisions, as well as specific improvements that have been made. Most of these features were in the Merb router, but the Rails DSL is more fully developed, and the fuller emphasis on Rack is a strong improvement from the Merb approach.
Improved DSL
While the old map.connect DSL still works just fine, the new standard DSL is less verbose and more readable.
# old way ActionController::Routing::Routes.draw do |map| map.connect "/main/:id", :controller => "main", :action => "home" end # new way Basecamp::Application.routes do match "/main/:id", :to => "main#home" end
First, the routes are attached to your application, which is now its own object and used throughout Railties. Second, we no longer need map, and the new DSL (match/to) is more expressive. Finally, we have a shortcut for controller/action pairs ("main#home" is {:controller => "main", :action => "home").
Another useful shortcut allows you to specify the method more simply than before:
Basecamp::Application.routes do post "/main/:id", :to => "main#home", :as => :homepage end
The :as in the above example specifies a named route, and creates the homepage_url et al helpers as in Rails 2.
Rack It Up
When designing the new router, we all agreed that it should be built first as a standalone piece of functionality, with Rails sugar added on top. As a result, we used rack-mount, which was built by Josh Peek as a standalone Rack router.
Internally, the router simply matches requests to a rack endpoint, and knows nothing about controllers or controller semantics. Essentially, the router is designed to work like this:
Basecamp::Application.routes do match "/home", :to => HomeApp end
This will match requests with the /home path, and dispatches them to a valid Rack application at HomeApp. This means that dispatching to a Sinatra app is trivial:
class HomeApp < Sinatra::Base get "/" do "Hello World!" end end Basecamp::Application.routes do match "/home", :to => HomeApp end
The one small piece of the puzzle that might have you wondering at this point is that in the previous section, I showed the usage of :to => "main#home", and now I say that :to takes a Rack application.
Another improvement in Rails 3 bridges this gap. In Rails 3, PostsController.action(:index) returns a fully valid Rack application pointing at the index action of PostsController. So main#home is simply a shortcut for MainController.action(:home), and it otherwise is identical to providing a Sinatra application.
As I posted before, this is also the engine behind match "/foo", :to => redirect("/bar").
Expanded Constraints
Probably the most common desired improvement to the Rails 2 router has been support for routing based on subdomains. There is currently a plugin called subdomain_routes that implements this functionality as follows:
ActionController::Routing::Routes.draw do |map| map.subdomain :support do |support| support.resources :tickets ... end end
This solves the most common case, but the reality is that this is just one common case. In truth, it should be possible to constrain routes based not just on path segments, method, and subdomain, but also based on any element of the request.
The Rails 3 router exposes this functionality. Here is how you would constrain requests based on subdomains in Rails 3:
Basecamp::Application.routes do match "/foo/bar", :to => "foo#bar", :constraints => {:subdomain => "support"} end
These constraints can include path segments as well as any method on ActionDispatch::Request. You could use a String or a regular expression, so :constraints => {:subdomain => /support\d/} would be valid as well.
Arbitrary constraints can also be specified in block form, as follows:
Basecamp::Application.routes do constraints(:subdomain => "support") do match "/foo/bar", :to => "foo#bar" end end
Finally, constraints can be specified as objects:
class SupportSubdomain def self.matches?(request) request.subdomain == "support" end end Basecamp::Application.routes do constraints(SupportSubdomain) do match "/foo/bar", :to => "foo#bar" end end
Optional Segments
In Rails 2.3 and earlier, there were some optional segments. Unfortunately, they were hardcoded names and not controllable. Since we’re using a generic router, magical optional segment names and semantics would not do. And having exposed support for optional segments in Merb was pretty nice. So we added them.
# Rails 2.3 ActionController::Routing::Routes.draw do |map| # Note that :action and :id are optional, and # :format is implicit map.connect "/:controller/:action/:id" end # Rails 3 Basecamp::Application.routes do # equivalent match "/:controller(/:action(/:id))(.:format)" end
In Rails 3, we can be explicit about the optional segments, and even nest optional segments. If we want the format to be a prefix path, we can do match "(/:format)/home" and the format is optional. We can use a similar technique to add an optional company ID prefix or a locale.
Pervasive Blocks
You may have noticed this already, but as a general rule, if you can specify something as an inline condition, you can also specify it as a block constraint.
Basecamp::Application.routes do controller :home do match "/:action" end end
In the above example, we are not required to specify the controller inline, because we specified it via a block. You can use this for subdomains, controller restrictions, request method (get etc. take a block). There is also a scope method that can be used to scope a block of routes under a top-level path:
Basecamp::Application.routes do scope "/home" do match "/:action", :to => "homepage" end end
The above route would match /home/hello/foo to homepage#foo.
Closing
There are additional (substantial) improvements around resources, which I will save for another time, assuming someone else doesn’t get to it first.

Laran Evans, Posted January 9, 2011, 1:22 am
Great writeup. Very helpful to see some examples.
Here’s a question though:
I know I can do this:
match ‘/transactions/search’ => “transactions#search”, :as => :search_transactions, :via => [:get, :post]
But is there a way to do something similar in a resourceful way as one entry?
resources :transactions do
member do
get :search
post :search
end
end
Is there a way to say I want a route called search that takes either a get or a post without having to put two entries in the member block?
Thanks!
sano, Posted March 7, 2011, 3:00 pm
I want a route called search that takes either a get or a post without having to put two entries in the member block?
yohimbine hcl