Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; he spends his daytime hours at the startup he founded, Tilde Inc.. Yehuda is co-author of best-selling jQuery in Action and Rails 3 in Action. He spends most of his time hacking on open source—his main projects, like Thor, Handlebars and Janus—or traveling the world doing evangelism work. He can be found on Twitter as @wycats and on Github.

The Rails 3 Router: Rack it Up

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.

52 Responses to “The Rails 3 Router: Rack it Up”

I am amazed by all the coon things that come out of all this new enhancements to rails routing, way to go!

Initially, I’m struck with the observation that several of these are just a shade away from other existing conventions in Ruby. Is there time to modify them to align with how code is already written in other Ruby applications?

For example, you have “main#home” to refer to the MainController’s home method. The convention in ri and other Ruby code is to match the class name (“MainController”) or at least capitalize it (“Main#home”). In Merb, controllers didn’t repeat the “Controller” name in every class name; they just used the shorter plural name (which matches how Rails REST routes are written and seems to be what you’re aiming for here).

And the explicit support for optional segments with parentheses looks almost but not quite like a regular expression. Why not just use a regular expression?

The syntax used in Rails has massive influence over the way many developers write Ruby code, even apart from Rails. If Rails introduces slightly different syntax, either people will be confused or existing conventions will lose out.

(Apologies if these issues have already been discussed and settled on public mailing lists elsewhere.)

I like what I see here, but what’s up with the :constraints sub-options hash. It seems like the benefit here is in the “constraints” block.

But I really don’t like sub-option hashes. I’m already giving options, and I really had a lot of newbie frustration with rails figuring out whether or not this or that option went in the :html sub-options or not.

The Merb router had it quite nice with all the constraints as top-level options. DataMapper does this well too, it even allows for a :conditions sub-option hash for cases where it makes the most sense.

Am I missing a really important reason the api is this way?

If “main#home” is just a shortcut for MainController.action(:home), which is a Rack application, and if in fact all the routes inevitably point to a Rack application — I mean let’s say you have 50 routes (all of which are “main#home” or the like). Won’t that mean 50 Rack application objects being loaded into memory at some point? That seems a bit redundant if that’s the case. But then again, maybe the objects are cheap and there’s nothing to worry about..

First of all, object allocation *is* in fact quite cheap. Consider the fact that every time you use a literal Hash in parameters you’re creating a gratuitous Hash object. Given that, a few extra objects (even 50) doesn’t matter at all.

That said, we cache endpoint objects, so it will result in 1 endpoint, not 50.

Great stuff. I’m sure we’ll be stealing some of these ideas for use in JSGI frameworks (http://jackjs.org/)… just remember, imitation is the sincerest form of flattery ;)

well, object allocation may be cheap, but GC isn’t, right?

It was noted that the endpoint object is being cached, so there aren’t a plethora to be GC’d. But even if there were, it seems unlikely that app endpoints would be collected while the app was running. More importantly, I’m sure Yehuda et al. have profiled the hell out of all of this stuff. (This is evidenced by all the performance numbers that have been released, as well as the team’s generally highly responsible approach toward development.)

Yehuda: More blogging! Loving learning about the evolution to Rails 3. The world must know about the changes to resources! ;-)

Sorry for the double-post, but speaking of saying more, is there more to be heard on the work with ActiveRecord/ActiveModel? ActiveRecord has seemed to be the least-discussed Rails component duing the last year of changes (very noticeably at last year’s RailsConf, for example.) Has there been significantly more difficulty than expected in achieving the modularity and decoupling for AR? More discussion on this (there was a brief bit in Jeremy Kemper’s recent ReR09 talk) would be pretty swell.

Great stuff, but I’ve got the same reaction as Geoffrey (topfunky) Grossenbach.

If possible it would be nice to align Rails conventions with general Ruby conventions.

Geoffrey’s comment was interesting. Interesting reply on the way? :)

are late-bound/deferred routes on the table?

By definition, since Rack endpoints are supported. In rack-mount, endpoints can return a 404 with a Rack-Cascade header set to “pass” to tell the router to continue with its search. This, for instance, will allow the router to route to Sinatra apps in cases where it’s possible that the route might not be found in the child app. You could trivially create a deferred method such that you could do match "/", :to => deferred { ... }

I am with Geoffrey’s 100%. These “issues” need to be properly discussed.

Great work with the routes BTW.

Love the routing changes. Will you be able to add another applications routes to the mix? Example, say you wanted to integrate Beast into your application:

Mine::Application.routes do; mount Beast::Application; end

Is there some syntax for working with these pre-packaged apps, similar to merb-slices?

I’m also concerned about the “foo#bar” notation. I don’t think this is used anywhere else in Rails and implies some magical parsing somewhere.

I prefer the explicit :controller => ‘foo’, :action => ‘bar’ for non-restful actions; not sure I understand why that doesn’t seem acceptable anymore. Maybe I’m missing something?

Thanks!

The new router looks great, thanks for your work on this Yehuda. I like the brevity of “foo#bar”. Whether it ultimately uses the # delimiter or a slightly different syntax, I am sure it will be easy enough to acclimate to.

Since I did the API design for this, allow me to elaborate on the constraints and concerns we were operating under.

First, main#index draws inspiration from two sources: 1) the pattern we’ve been using in Rails since the beginning of referencing controllers as lowercase without the “Controller” part in :controller => “main” declarations and 2) the Ruby pattern of signaling that you’re talking about an instance method by using #. The influences are even part mixed. Main#index would be more confusing in my mind because it would hint that an object called Main actually existed, which it doesn’t. MainController#index would just be a hassle to type out every time. Exactly the same reason we went with :controller => “main” vs :controller => “MainController”. Given these constraints, I think “main#index” is by far the best alternative (also, you can still use the old :controller/:action declaration, but it won’t be idiomatic Rails usage to do so).

Second, dropping “Controller” from the name of controllers is imo a very bad idea. It bleeds the naming spaces between the domain model and the UI (controller/view) in a way that’s not helpful. And it can potentially lead to naming conflicts. Calling your PostsController just “Posts” suggests that you’re talking about a collection class for posts, which simply isn’t true. Further more, it’s not unreasonable to think that you’ll actually have some container classes that would be named in plural.

Then just use namespaces, you could say. Go with Controller::Posts. I think that’s even more of a hassle without gaining anything. You add two colons, flip the natural reading order, AND you still suggest that the core class is a collection class, not a controller. No win.

Third, why aren’t routes regexes. The routing declarations have to work in reverse as well. We have to be able to generate a route from the original string. That’d be a lot more hassle with regexps. Besides, regexps supports a ton more features than we’re interested in supporting with routing. And finally, using the routes in a way that they are so advanced as to closely resemble a regexp will be by far a minority case. The big deal for the new router is how it focuses on making resource routing first-class and the primary approach to routing. The design split is a 95/5 split. You’re expected to use REST resources 95% of the time and non-REST routes just 5% of the time.

Hope that helps explain the reasoning behind this. We spent a lot of time thinking about this API. Probably more work went into designing this than any other new piece of API in Rails 3.0. Doesn’t make it automatically fantastic, just that we considered all of the questions raised above along the way in the design and still arrived where we did.

It’s worth adding that pure regular expressions have annoying chrome that makes things more annoying. Specifically, you’d have to use a character class that explicitly ignored “/”s and specify the ^ and $. Additionally, you’d have to do some work to map the numbered captures to parameter names.

Django takes the regular expression approach and even simple cases are cluttered with chrome and have no easy way to deal with named segments (you have to handle the regular expression work yourself manually, every time).

“Third, why aren’t routes regexes. The routing declarations have to work in reverse as well. We have to be able to generate a route from the original string.” – DHH

This actually works David :P

Rackmount has first class support for regexps and prefers 1.9′s new named capture syntax. The string expression is just a dsl for building Regexps.

Strexp.new(“/posts/:id(.:format)”)
=> /\A\/posts\/(?.+)(?:\.(?.+))?\Z/

Yes, it generates too.

@josh yep — that’s what I meant when I said you wouldn’t want to generate the regexen by hand :P

I also enjoy the brevity of “foo#bar” it’s pretty neat! but as topfunky said before … shouldn’t it be “Foo#bar”? Assuming MainController is a constant and bar a method. Why the difference?

Great stuff Yehuda! Can’t wait for Rails 3

Very good points. Thank you DHH and wycats to take the time to explain it so clearly.

Ops… sorry for my previous post.. haven’t seen DHH’s reply.

Can I match the subdomain just like any other part of the URL? If not, why not? I don’t want to use a constraint, I just want to pattern match it (e.g. to get a username value from http://someuser.photos.com/ ). Thanks for considering my feedback.

What’s the thinking behind still making the old map.connect DSL available in Rails 3 versus making a clean break? Will it trigger deprecation warnings?

Questions mentioned above aside, I really like the new expanded constraints. Unless I’m missing something here this seems like it will alleviate some of the existing routing issues. To me it just seems to be an easier, but more expressive, way of defining your routes.

Thanks for the post, Yehuda (and thanks for the clarifications, David). And one comment made above states, “more blogging.” :)

i feel strongly with geoffrey’s comment. the new idiom “post#show” is interesting, but also a bit confusing: it is moving away a bit from ruby. using strings to overcome limitations of a programming language. in java i see a lot of use of strings that are parsed and used as bits of programming code in order to hide the language’s inflexibility.

why not: :to => PostsController.show
or even: :to => Posts.show

@DHH you say name clashes are the reason for putting “Controller” after controller class names.. is it not possible to use ruby’s namespace features to put all controller classes in a Controller:: namespace? i think namespaces are ultimately the ruby way to avoid clashes, adding words to class names conflict with DRY, smells like C and again ivents new idiom for something ruby already has idiom for.

i understand that Posts.show does not return a Rack application, but maybe we can allow controllers to be in 2 modes: “setup mode” in which its methods return Rack apps, or “live mode” in which they return responses.

“:to => Posts.show” is the prettiest solution i can think of. and i think it is possible bring this idiom to rails.

@geoffrey you’re right about the convention. but even that convention has not really been used yet _inside_ ruby code. i dont think it needs to be used in ruby code at all. in ruby we address a class’ method with a “.” not a “#” inside a string — we dont need another way to address a class’ method.

@geoffrey i totally agree with the “rails influences other ruby code” statement. therefor rails should stick to standard ruby and not invent new idiom for something ruby has already idiom for.

my bottom line: i love the work you guys are doing, i’m not able to match it, so i’ll use it when it comes out no matter what. the argument i make is driven by my purist side. the bottom line is: im pragmatic.

@cies: but what happens when you have an object named Posts, which may or may not contain a show method? how would rails know which to choose from.

and your statement “using strings to overcome limitations of a programming language”, I’m not really sure that’s their aim here at all. strings are being used as a shortcut instead of writing out the verbose `:controller => ‘posts’, :action => ‘show’`. and the whole “controllers acting in two different modes (rack / live)” is already covered. yehuda’s previous entry demonstrates that “posts#show” will return PostsController.action(:show), which is a rack endpoint

some magic is going to happen, yes. be it either with using “posts#show”, Posts.show, or PostsController.show, it just so happens that using strings is the least troublesome approach

what I am curious about is: will this string convention be used all throughout Rails 3? i.e, would I be able to write `redirect_to “posts#index”` and `link_to “sign in”, “sessons#new”`

At the risk of sounding like an idiot, what about the notion of a “no route” method.

Real world example: I have a flashplayer that is responsible for loading another flash file to play. Rails insists of routing this second file. So what about a “Nomatch” method?

@hal I’m not sure what you’re getting at. Can you explain better?

Sure.

Simple real world example. I have a flash app embedded in my rails app. This flash app loads another .flv file, sometimes from from S3. Rails sees the amazon S3 file read as something to route since it is in the form of http://xxx.com.

To avoid errros, I have to put a little code in the controller/method to detect this special case, and re-render the page. I could filter in my web server, but what if I’m not running one — heroku? I guess I could write a rack filter, but then, isn’t that what the new routing implementation is?

What would be nice is an anti-route — never_match ‘xxx.com’

I’ll freely admit there might be a better way, but if it exists, my google skills and rails library isn’t willingly offering up a solution.

Hal

> what happens when you have an object named Posts,
> which may or may not contain a show method? how
> would rails know which to choose from.

@mike if all controller classes are defined in the context of the Controllers module, then you can execute the route setup code in that context as well. (correct me if im wrong)

> I’m not really sure that’s their aim

@mike “:controller => ‘posts’, :action => ‘show’” is also using strings to sidestep from ruby. “:controller => Posts, :action => :show” would be more rubyesque in my opinion.

> yehuda’s previous entry demonstrates that “posts#show” will
> return PostsController.action(:show), which is a rack endpoint

@mike “posts#show” will be parsed into PostsController.action(:show). that idiom, if we dump the Controller auxillaries (so Posts.action(:show)), is short enough for me. bringing some new string+parsing idiom to the game just makes it more bloated and opaque.

(please forget about the setup/live thing i said, Posts.action(:show) is more than short enough for me)

> some magic is going to happen

@mike actually with Posts.action(:show) not much magic happens, and the idiom is very rubyesque.

> would I be able to write `redirect_to “posts#index”`
> and `link_to “sign in”, “sessons#new”`

@mike you should be if this syntax become default. i’d obviously prefer:

redirect_to Posts.action(:index)
link_to “sign in”, Sessons.action(:new)

but then rack apps need to be resolvable to urls, and i dont know if that’s possible.

again: i’m not fighting the new idiom, i just think it’s a bit of a pity we have to go down the road of using parsed strings to program, instead of plain ruby. for instance, in the new idiom, how to use a nested controller? “admin::posts#show”?

Yummy :) I have only one question: When? When will you release the Rails 3? :)

@Hal Why don’t your S3 URLs start with either bucket.s3.amazonaws.com/… or s3.amazonaws.com/bucket/… ?

Looking fantastic guys.

And thanks for the elaborate explanations on why you went with the design.

I second the question of how nested controller strings will look like though.

I cannot find any “Basecamp” module in Rails3 edge source code. Is it not added to repos yet? Or should I search it in different Git branch, experimental, or what?

@Elliot,

The do, well, really http://s3.amazonaws.com/bucket/

The key thing is I don’t want this URL processed by rails at all. Basically, anytime we embed any other application in rails, there is the possibility of that app requsting a URI — so I don’t think this example is, or will be, that unusual.

A lot of work is expended in Rails apps trying to identify/reconstruct the REST resources implied by the URI. There are several plugins (such as Ian White’s excellent ResourcesController and another author’s ResourceController) that exist primarily to accomplish this task.

Unfortunately, in Rails 2, it’s difficult to exploit the excellent modeling made available by ‘map.resources’ and the only solution is to parse the URI path.

My Question: In Rails 3, will it be possible to access the matched route adorned with the hierarchy of routing resources (for the 95% case when a REST route is matched) from the controller? This would lower the barrier to achieving “Full-Circle REST” and writing resource-oriented applications.

Now that the Rails router can directly expose Rack endpoints, what does this mean for Metal?

Hi,

I played with Rails 3.0pre today and when I tried to use:

resource :articles, :only => [ :index, :show ]

The new router ignore the rules and create all resources urls.

Should the only and except rules be removed in 3.0 or is this a not yet implemented thing?

I second the question about only. Currently results in a crash.

resources :xxx, :only => :create

undefined method `map’ for :create:Symbol

rails actionpack/lib/action_dispatch/routing/mapper.rb:370:in `actions’

I’d love if controller can route themselves in Rails 3, similar to how Ramaze does it.

Is there a way to generate a list of routing helpers left behind by the router? Just like rake routes, but includes the _path(s) and _url(s) for use in controllers and views? That would be a helpful thing if this was just added to rake routes……most especially in nested resource situations.

Yes it’s easy to know what your helpers are if you are not in a nested resource situation.

It seems bizarre to me that rake routes will list the routes but not how to get at them. The how to get at those routes is the helpers, and depending on relationships that can be more of an adventure than it should be to figure out what_is_the_helpers_name_and_nested_resource_with_id_and_what_object_or_id_to_pass…..
It seems like there is a lot of effort at explaining what the route is (yes it’s a url mapped to an abstraction) but there is little discussion of how to actually get there. Seems like Rails already knows, so why do we need to keep it a secret….is this a snake or a trunk…..is this a tree or a leg…..

Could rake routes list the named helpers please?? …parent_child_id_action_url_or_path listed along with the rest of rest of the info in rake routes…..

How would you go about specifying a constraint that would only route when there is no subdomain? I am using:

constraints(:subdomain => ”) do

end

The routes are still available when a subdomain is present which seems wrong.

I like the new routes in general, but I cannot get it done with controllers inside modules. For example I have modules “site” and “admin”, with controllers inside each of them. I’m not RESTING, so I just use in my routes.rb:
match ‘:controller(/:action(/:id(.:format)))’

In Rails 2.x — “site/security/login” matched to “site/security#login” out of the box – but I cannot make it running with Rails 3:
rs = ActionController::Routing::Routes
rs.recognize_path “/site/security/login”
# No route matches “/site/security/login”

I cannot even generate the url for it:
rs = ActionController::Routing::Routes
rs.generate :controller => “site/security”, :action => “login”
# ActionController::RoutingError: No route matches {:controller => “site/security”, :action => “login”}

What am I doing wrong?

Nevermind, it’s fixed in Edge :)

I just love writing router syntax now. It feels more like an english while I am writing a route. Thanks dhh and the rails team for getting this design to reality.

Is it possible to have a variable namespace? I have restful resources like the following:

resources :articles
resources :persons

But I need to scope these inside a variable namespace, such that it responds to URLs of the form:

‘:edition/:controller/:action/:id’

for example:

/foobar/article/edit/123 or /bazbam/person/edit/345

for each of the resources. Is this possible with the resources method, or must I hand-craft these? I will not know the possible values for :edition ahead of time; these get looked up in a before_filter in my ApplicationController.

Leave a Reply

Archives

Categories

Meta