What's New in Edge Rails: Shallow Routes

Posted by ryan
at 4:32 PM on Sunday, September 07, 2008

Rails’ routing mechanism is pretty slick. In a very intuitive way you’re able to describe the resources you want exposed at the URL level with this routing-DSL:

1
2
3
4
5
map.resources :users do |user|
  user.resources :articles do |article|
    article.resources :comments
  end
end

However, while this configuration makes an article available at /users/1/articles/1 and comments at /users/1/articles/1/comments/1 there are often times when you want to bypass the full nested hierarchy and directly access the resource in question. Now, with the shallow route option, you can.

1
2
3
4
5
map.resources :users, :shallow => true do |user|
  user.resources :articles do |article|
    article.resources :comments
  end
end

This configuration removes all nested member paths (like /articles/1/comments/1) and makes them directly accessible (as /comments/1). Basically, routes are built only with the minimal amount of information that’s needed to uniquely identify the resource.

1
2
3
4
user_articles_path(1) #=> '/users/1/articles'
article_path(2) #=> '/articles/2'
article_comments_path(3) #=> '/articles/3/comments'
comment_path(4) #=> '/comments/4'

No longer do you need to wrestle with infinitely nested routes, the :shallow option automatically makes all routes as concise as possible.

tags: ruby, rubyonrails

Comments

Leave a response

  1. joostSeptember 07, 2008 @ 05:00 PM

    This is so useful I’d rather have it be the default.

  2. Josh NicholsSeptember 07, 2008 @ 05:08 PM

    I think you have a typo in line 1 of the second code snippet: should be :shallow, not :shallpow.

  3. HonzaSeptember 07, 2008 @ 05:30 PM

    Hey, cool feature. By the way, I think you got a typo there: map.resources :users, :shallpow => true do |user| # should be :shallow, right?

  4. Chris LloydSeptember 07, 2008 @ 06:23 PM
    Few typos:
    • article.resourcs :comments
    • map.resources :users, :shallpow => true
  5. Officer GrammarSeptember 07, 2008 @ 06:51 PM

    map.resources :users, :shallpow, huh?

    sounds like a promise that your code will explode :p

  6. Dmytro ShteflyukSeptember 07, 2008 @ 07:32 PM
    Hey, you have an error in example:
    map.resources :users, :shallpow => true do |user|
    Should be “shallow”

    PS. Great feature, thanks for sharing!

  7. Javan MakhmaliSeptember 07, 2008 @ 09:52 PM

    I look forward to these posts. I assume you meant :shallow => true, and not :shallpow..

  8. Eric CranstonSeptember 07, 2008 @ 10:57 PM

    In the second code example you put ”:shallpow => true, you meant => true”

    Thanks for these posts. Nice to keep up without having to review all the commits. :)

  9. sebSeptember 08, 2008 @ 01:17 AM

    Great post and great features Thanks Ryan

    I think there is just a little typo with shallpow instead of shallow :)

  10. BradSeptember 08, 2008 @ 01:25 AM

    There are a bunch of typos in your second code example.

  11. Jeremy GailorSeptember 08, 2008 @ 02:57 AM

    Type-o:

    I’m guessing “map.resources :users, :shallpow => true do |user|” should be “map.resources :users, :shallow => true do |user|”

    Otherwise, cool feature. I think it will be both handy to those comfortable with rails routes, and a little bit more confusing for those not.

  12. Tom-EricSeptember 08, 2008 @ 02:57 AM

    It seems you have a typo in your second code block (:shallpow instead of :shallow).

  13. AntoninSeptember 08, 2008 @ 03:09 AM

    Really interresting, as usual. Although, It seems there is some typos in code examples.

  14. TomashSeptember 08, 2008 @ 04:06 AM

    “shallow” in second code block has accidentally changed into “shallpow” ;)

    Rails 2.2 are going to kick some serious butt. Keep up the good work Ryan, there’re lot of people getting edge rails info from your blog.

  15. ChrisSeptember 08, 2008 @ 04:17 AM

    In the second routing example, :shallpow should :shallow. Otherwise, great post! Thanks Ryan!

  16. Stephen BrownSeptember 08, 2008 @ 04:30 AM

    Looks like your fingers have been hitting several buttons: Line 3 on both examples: :resourcs should be :resources, and :shallpow should be :shallow, I think?

    Nice article, thanks

    Steve

  17. DaveSeptember 08, 2008 @ 06:19 AM

    Typo on line 3 in each of your first two examples: articles.resourcs.

    Also, thanks!

  18. Dr NicSeptember 08, 2008 @ 07:03 AM

    TODO ”:shallpow => true => => true” – in 2nd code block

  19. Dr NicSeptember 08, 2008 @ 07:07 AM

    This is very cool and should make some of my routes files much shorter.

  20. PhilSeptember 08, 2008 @ 07:57 AM

    Think you must have hit ‘p’ when you typed :shallow => true

  21. YoNoSoyTuSeptember 08, 2008 @ 09:15 AM

    Just a little typo: you have written ”: shallpow” in your code snippet.

  22. BenSeptember 08, 2008 @ 09:20 AM

    Another great tip Ryan! Just to let you know, there is a typo in your second code block.

  23. Rafa G.September 08, 2008 @ 09:53 AM

    It’s great. Please, correct a little errata in:

    article.resourcs :comments

    Should be:

    article.resources :comments

  24. jmSeptember 08, 2008 @ 10:11 AM

    typo s/shallpow/shallow in second snippet

    thanks for the write up :)

  25. spencerSeptember 08, 2008 @ 11:28 AM

    map.resources :users, :shallpow => true do |user| line 1: should be w ?

  26. ScotySeptember 08, 2008 @ 12:37 PM

    I may be missing something here, but…

    Aren’t you going to have to do a bunch of funny business in the controllers to discern if the request is coming from a shallow or full route?

    In cases like this, I’ve been doing something like this:
    map.resources :users do |user|
      user.resources :articles, :controller => 'user_articles'
    end
    
    map.resources :articles
    

    user_articles_controller.rb assumes that user_id will be defined in params while articles_controller.rb does not. Its a pain to have two controllers, but I think that’s better than having a bunch of logic in the controller to discern what to load.

    So I don’t quite see how the “shallow” option helps since I need to specify different controllers for the routes.

    Or…am I doing something the hard way?

  27. Luis LavenaSeptember 08, 2008 @ 12:55 PM

    Nice, but what about content duplication that play negatively with search engines?

    Also, it will not play nice if the nested resource is expecting something from parent one, which sometimes is the objective of nested resources in the first place :-)

    (or I’m missing something?)

  28. CarlSeptember 08, 2008 @ 01:11 PM

    There are a couple of typos in the first two code samples. “resourcs” and “shallpow”.

    This sounds like a nice shortcut to route configuration.

  29. gSeptember 08, 2008 @ 01:41 PM

    shallpow typo

  30. Trevor TurkSeptember 08, 2008 @ 01:52 PM

    That second code block has a typo on the first line, methinks. (“shallpow”)

  31. Douglas F ShearerSeptember 08, 2008 @ 02:21 PM

    - articles.resourcs :comments + articles.resources :comments

    Sorry to be so pedantic!

  32. Jordan ArentsenSeptember 08, 2008 @ 02:48 PM

    I think “shallpow” is supposed to be “shallow”.

  33. PeterSeptember 08, 2008 @ 08:12 PM

    Great idea! Ryan, just a little type in your code :shallow => true instead of :shallpow => true Thanks, Peter.

  34. Giles BowkettSeptember 08, 2008 @ 09:12 PM

    :shallow => true

    not

    :shallpow => true

  35. Grant HutchinsSeptember 09, 2008 @ 01:21 AM

    Typo: :shallpow -> :shallow

  36. MaciejSeptember 09, 2008 @ 04:53 AM

    Ryan, there is a small typo Second example, 1 line should be :shallow instead of :shallpow.

  37. Ryan DaigleSeptember 09, 2008 @ 08:32 AM

    Sorry about that guys – my akismet config was broken meaning that all your comments were dumped in some sort of pergatorial state. Thanks for the typo fixes – I think I got the message :)

  38. joostSeptember 09, 2008 @ 08:46 AM

    @ Scoty: you’ve been doing things the hard way!

    The idea is that both the nested routes and the top-level routes go to the same controller.

    In that controller, what I always do, is try to determine the top-level model based on the params. I put this in a before_filter:

    @user = User.find params[:user_id]

    When it’s nil I know I’m in the top-level route, when it’s set I’m in the nested route.

  39. ScotySeptember 09, 2008 @ 11:21 AM

    @joost: that makes sense, but…

    Your redirect_to’s are going to be different depending on the nesting status (articles_path vs. user_articles_path(@user), etc.) Do you put a bunch of if statements to account of the differences?

    And what about the views? All the links and form targets are going to vary depending on whether your working with a nested route or not.

    It seems messy to have a bunch of conditions in your controllers and views.

    Or is there a more graceful approach that I am not seeing?

  40. ibrahim ahmedSeptember 10, 2008 @ 07:21 AM

    I don’t think shallow routes option is a good thing. It will make an additional redundant entry url for the article pages. is it a good thing to have two url refer ti the same page.

  41. eleanor higginbothamSeptember 10, 2008 @ 12:08 PM

    excuse me sir but i think there might be a typo!

  42. YvesSeptember 11, 2008 @ 03:18 PM

    I think that with shallow set to true no map.resources :articles etc. should be needed, so please correct me if I’m wrong… but it seems that no formatted_ path pendants are created so the following does not work if I need e.g. formatted_articles_path somewhere.

    map.resources :artist_profiles, :shallow => true do |artist| artist.resources :articles do |article| article.resources :comments end artist.resources :releases artist.resources :events do |event| event.resources :member_photo_albums end artist.resource :playlist artist.resource :photo_album end

  43. Andrew TurnerSeptember 12, 2008 @ 09:22 AM

    I’ve always built this concept of nested resources still being available as first-order resources as well. However, as @Scoty points out there are issues with funniness in the controller doing detection on how the resource is being viewed, as the first-order resource, or the nested one.

    I would be interested in best practices about how to handle this in elegant ways – and not just assuming it’s always a nested resource of users, but that many resources can have other nested resources, and can vary.

  44. ngumaSeptember 12, 2008 @ 12:07 PM

    Nice work Ryan. I have a question though. Pretend you need to want to page and sort those results resfully: ’/users/page/1/by/name’ ‘users/1/articles/page/1/by/date’ ‘users/1/articles/28/comments/page/1/by/date’

    Is there a way to apply those to any level of the hirarchy?

  45. z0rroSeptember 18, 2008 @ 08:51 AM

    You morons! How many times you want to point one stupid typo?! RTCF – Read The Comments First!

  46. aprendizbasicoSeptember 23, 2008 @ 10:30 AM

    Hi Ryan,

    These routes are not generated :

    articles_path #=> ’/articles’ comments_path #=> ’/comments’

    This is probably a bug.

    Thanks.

  47. S. Brent FaulknerSeptember 25, 2008 @ 01:04 PM

    As the author of the feature, I’m here to clear up a bit of confusion…

    When you use the shallow option, it does not generate both routes… any routes that reference an explicit id will not require the parent id, but any routes that do not will.

    For example, given the above example of users/articles/comments…

    The path for all user would be: /users The path for a specific user would be: /users/1

    The path for all articles for a user would be: /users/1/articles The path for a specific article would be: /articles/2 This is instead of /users/1/articles/2 (which is what it would be without the shallow option)

    Similarly…

    The path for all comments for a given article would be: /articles/2/comments The path for a specific comment would be: /comments/3

    If you want a route for ALL comments (or ALL articles) you’d need to explicitly map it.

    One other enhancement that goes along with this is the :has_many option has been extended to support nesting in a similar manner to the :include option in ActiveRecord finds.

    e.g. map.resources :users, :shallow => true, :has_many => { :articles => :comments }

    cheers.

  48. Ryan DaigleSeptember 30, 2008 @ 07:17 AM

    Thanks for the clarification, Brent – I’ve cleaned up the post.

  49. Hattie AnnOctober 02, 2008 @ 08:27 PM

    Now, Now, gentlemen. There’s no point in getting so angry over a simple mistake, now is there? It’s fixed now so let’s all be thankful for no more bickering, okay? Great save there Ryan!!!

  50. VipinNovember 01, 2008 @ 01:59 AM

    Hi All,

    i want to create a question. As per your eg …

    user_articles_path(1) #=> ’/users/1/articles’ article_path(2) #=> ’/articles/2’ article_comments_path(3) #=> ’/articles/3/comments’ comment_path(4) #=> ’/comments/4’

    Is it right? user_articles_comments_path(3) #=> ’/users/3/articles/3/comments/3’

    If i want a relation like this then how can we create it? ’/users/5/articles/8/comments/12’ ??

    Thanks in advance… Vipin