Named, explicit routes

Posted by Jamis on January 22, 2007 @ 01:36 PM

Names routes are hotsauce. We are all forever indebted to Marcel Molina, Jr. and his gift for elegant API’s. Without him, we’d still be stuck in the quagmire that is “map.connect”.

These days, I don’t use map.connect at all. Named routes for everything. Does that seem too extreme?

Let me clarify a bit. I don’t use map.connect with implicit routes. And just what is an implicit route? This is:

1
map.connect ":controller/:action/:id"

That’s an implicit route, because it implicitly maps any number of paths to any number of controllers and actions. Contrast that with an explicit route:

1
map.connect "/people/:id", :controller => "people", :action => "show"

The controller and action are now hardwired into the route. And if you’ve gone that far, you might as well go with a named route, too, since you can then access it directly:

1
map.person "/people/:id", :controller => "people", :action => "show"

So, why do I favor explicit routes over implicit routes? Consider some of the issues with implicit routing:

  • If you want to temporarily “turn off” one of your controllers (say, you’re having problems with your RSS feeds and need to disable them for a bit), you have to somehow make sure the controller file can’t be found by the routing code, which means removing it, obfuscating it’s name, or some other hack. Using named routes, you just comment out the routes that point at the controller in question, and you’re all set.
  • Say your application is a year old or so (ancient!) and has an established base of users, who have all bookmarked various areas of your app. Now, you go and refactor things, so that an action that used to be in one controller is now in another, and so forth. With implicit routes, you’re hosed, but with explicit routes your users’ bookmarks will continue to function as before, since the route that gets recognized remains unchanged—it simply points to a different place now.
  • Implicit routes are extremely concise to define, but very verbose to use, since you often have to give the controller and action in the options to link_to, or url_for, or whatever you use. Sure, your routes.rb file is only one line long, but how much more did you have to type in your views?

Furthermore, I prefer named routes over unnamed routes, for a few reasons.

  • Route generation using implicit routes is ugly, both internally and externally. I mean, seriously, who wants to type “link_to(person.name, :controller => ‘person’, :action => ‘show’, :id => person)” when they can type “link_to(person.name, person_url(person))”. It gets even worse if you want to pass HTML options to link_to, because you have to use explicit curly braces around the route options in that case. Named routes are, as I said, hotsauce.
  • You can use named routes in functional and integration tests. Pretty cool! In functional tests, you can do “assert_redirected_to(person_url(people(:bob)))”. In integration tests, you can say “get person_url(people(:bob))”. This hearkens back to the refactoring issue—if you change where your actions are, your integration tests remain unaffected.

“But,” you say, “explicit route definitions are so much more verbose than implicit route definitions!”

Sure, if you do it all long-hand. But if you use Object#with_options, it collapses nicely:

1
2
3
4
map.with_options :controller => "people" do |people|
  people.people "/people",     :action => "index"
  people.person "/people/:id", :action => "show"
end

Using explicit, named routes, your routes.rb file now reads like a description of what your application can do. You have ULTIMATE CONTROL (mwa-ha-ha-ha) over what parts of your code visitors can hit, and how they hit it. And you can refactor with impunity, too!

Think of it like this: a route is not the name of your action. It is an alias for your action, and as such need not have any relationship to the actual implementation.

Posted in Tips & Tricks

Comments

Have something to add? Click here to leave a comment.

22 Jan 2007

1. Eric said...

I thought the point of that great implicit route was Convention over Configuration. Isn’t that what DHH has taught us?

Is the next evolution in routing going to be automatic creation of the “controller_url” helpers for you with only a default implicit route?

2. James H said...

Maybe it’s because I’m sick today, but a good chunk of that went over my head, and then you drop this on me:

I mean, seriously, who wants to type “link_to(person.name, :controller => ‘person’, :action => ‘show’, :id => person)” when they can type “link_to(person.name, person_url(person))”.

I think it’s about time I pick up my Agile book again.

3. Jamis said...

Eric, sure. That’s what “map.resources” is all about. It puts a convention around all of the above.

But even for the places where it falls apart (like some oddball controller that isn’t RESTful), I think it’s better to be explicit than not. Convention over Configuration is great and all, but at some point you need to take charge of your destiny, man! :)

Seriously, though, if none of the above arguments convinced you, then you’ve obviously felt none of the pain I have with the implicit routes. I do what I do because of frustrations I’ve had with the default route.

4. Jon Leighton said...

Wow, thanks for this, I never thought about that.

5. Martin S said...

You write so much great stuff you could print your blog and publish it as a “rails tricks”-book. Thanks

6. Eric said...

Jamis, I wasn’t really intending to downplay the pain that is felt.

The thing that struck me was when you mentioned routes as being aliases to your actions—which I think is true, but made me think of the Java world that was shunned for all of its numerous layers of indirection.

I don’t mean to say it isn’t useful, or even that it shouldn’t be used, but it is intriguing to see that even in rails there are places where indirection and explicit configurations are both helpful.

What I think all of this comes down to is that it shows that rails has created a nice compromise as well as a nice migration path to allow someone to start with a simple informal application and allow it to grow to something much more structured when the time is right.

7. Jamis said...

Note that indirection (in moderation) is not a bad thing—it’s a crucial part of building any software. Java has just made so many best-practices so hard to do that they taste bad merely by association.

And you have a very good point, Eric: the default, implicit route is a great pedagogical aid, since it allows newcomers to Rails to get up and moving without having to learn anything about routing. There is definitely value there. I feel the same way about scaffolding. However, as with any crutch, there comes a time when you need to learn how to walk without it, and I guess that’s what I’m trying to communicate with this post.

Thanks for helping to clarify and focus my thesis!

8. Eric D said...

Hi Jamis!

Let me please thank you for this wonderful blog & the railsway, I’ve got the impression that I never learned that much about a language/framework. :)

Though, I still don’t have the skill to understand map.with_options :controller => "people" do |people| ...

api.rubyonrails.org didn’t help me much on this one. Could you please explain a bit more what are the corresponding URL/params?

Thanks again, Eric (another one)

9. Nate said...

Hotsauce?

You don’t mean “steaksauce” do you? You know, it’s A1! :)

10. Jamis said...

Eric, very odd, looking in the ActiveSupport code it looks like the with_options stuff has been nodoc’d, so it won’t show up in the API docs. I’ve removed that nodoc and added some documentation for with_options, so in the NEXT release, it’ll show up.

A quick rundown of how it works—basically it just lets you specify common options and then yield to a block. The block recieves a proxy object, which passes all methods through to the original receiver, and merges the options into any option hashes.

An example is worth a thousand words:

1
2
3
4
5
6
7
8
map.connect "/people", :controller => "people", :action => "index"
map.connect "/people/:id", :controller => "people", :action => "show"

# is the same as this:
map.with_options :controller => "people" do |people|
  people.connect "/people", :action => "index"
  people.connect "/people/:id", :action => "show"
end

11. Eric D said...

Thanks again!

So basically, it adds :controller => “people” to everything inside the block. :)

And if you change people.connect to people.list, you’ll get “redirect_to list_url” available through ActionView/Controller, right?

Bye & please keep up the good work! Eric

12. Doug said...

Wait a sec, this person_url method: where did it come from? The named route?? Could it be?

13. floyd said...

Doug, yes, map.person ‘people/:name’, :controller => “people”, :action => “show” configures a route which can be generated with the method person_url. Be careful, these can be overwritten. I second the point of this article, and to reiterate, named routes are sanctified as convention in 1.2 as ‘paths’. Namely, map.resource :people gets you a whole slew of pretty things like new_person_path. See the recent announcements on the Rails weblog etc.

These are very pretty, but traditional named routes will continue to be relevant because of the things Jamis says above. As for the convention v. configuration issue, I am surprised the issue of client requirements has not come up. So called pretty URLs are nearly industry standard fare in the specs I’ve seen recently. Named routes let you do this easily:

map.login "login", :controller => "sessions", :action => "new" link_to "Login!", login_url

Conventions are great if you work for 37signals and get to invent requirements for your customers, but for the rest of us configuration is going to figure heavily into any work we do with Rails. Luckily, named routes lets us do this elegantly while also maintaining our restful, maintainable backend and stay happy.

14. Joe said...

This was a great write-up. I’ve also been in love with named URLs and map.resources since I switched to Rails 1.2. About “switching off” controllers, though – if you uncomment a named route, won’t all your views/controllers/tests that the route_url method fail?

23 Jan 2007

15. Paul O'Shannessy said...

I just started using named routes in combination with a RESTful app, using them to shorten some URLs… login, logout, etc.. The with_options thing was something I haven’t seen before and I’m glad you showed it.

I’ve been following your recent flurry of posts and they’ve all been really helpful. Thanks a lot!

16. Jamis said...

Joe, true, and a good point. I could have done better illustrating the cases where that is actually useful. A better use-case would be temporarily pointing a URL at some dummy action, rather than disabling it entirely. That way the named routes will still exist, they’ll just be pointing at a different location.

Still, if you’re dealing with a URL that isn’t explicitly referenced in any views (like a hidden admin page, or a web-service, etc.), it may be that disabling the route entirely is what you want after all.

17. Doug said...

Great, thanks floyd, and thanks again Jamis for another great article. Really enjoying these.

24 Jan 2007

18. Adam T. said...

Named routes are great, but I still feel like you can get down into a rabbit hole where you can see some obvious DRY capabilities.

Take, for instance, this block of code:

1
2
3
4
5

map.with_options(:controller => "people") do |person|
  person.people("/people", :action => "list")
  person.person("/people/:id", :action => "show")
end

If you start duplicating that (and let’s face it: having named routes for all our basic actions would be great), you might be better off doing something like this:

1
2
3
4
5
6
7
8
9

["person", "item"].each do |cntl|
  eval <<-STR
    map.with_options(:controller => "#{cntl.pluralize}") do |obj|
      obj.#{cntl.pluralize}("#{cntl.pluralize}", :action => "list")
      obj.#{cntl}("#{cntl.pluralize}/:id", :action => "show")
    end
  STR
end

That way, commenting out an entire controller’s worth of named routes is as easy as commenting out one element in an array.

P.S. I’m trying out Mephisto’s syntax highlighting here… I pray it works like I want it to.