Nesting resources

Posted by Jamis on February 05, 2007 @ 01:00 PM

The RESTful routes feature in Rails makes it really, really simple to nest resources within each other. Just give a block to the “map.resources” call, and define further resources on the value yielded to that block:

1
2
3
4
5
6
7
map.resources :accounts do |accounts|
  accounts.resources :people do |people|
    people.resources :notes do |notes|
      notes.resources :comments
    end
  end
end

That monstrosity would allow you to define routes like:

1
2
3
4
5
6
7
8
accounts_url         #-> /accounts
account_url(1)       #-> /accounts/1
people_url(1)        #-> /accounts/1/people
person_url(1,2)      #-> /accounts/1/people/2
notes_url(1,2)       #-> /accounts/1/people/2/notes
note_url(1,2,3)      #-> /accounts/1/people/2/notes/3
comments_url(1,2,3)  #-> /accounts/1/people/2/notes/3/comments
comment_url(1,2,3,4) #-> /accounts/1/people/2/notes/3/comments/4

Simple! However, in using RESTful routes more and more, I’m coming to realize that this is not a best practice. Rule of thumb: resources should never be nested more than 1 level deep. A collection may need to be scoped by its parent, but a specific member can always be accessed directly by an id, and shouldn’t need scoping (unless the id is not unique, for some reason).

Think about it. If you only want to view a specific comment, you shouldn’t have to specify the account, person, and note for the comment in the URL. (Permission concerns can come into this, to some degree, but even then I’d argue that judicious use of the session is better than complicating your URLs.) However, if you want to view all comments for a particular note, then you do need to scope the request by that note. Given the above nesting of routes, I’m finding the following a better (if slightly more verbose) method:

1
2
3
4
5
6
7
8
9
10
11
12
13
map.resources :accounts do |accounts|
  accounts.resources :people, :name_prefix => "account_"
end

map.resources :people do |people|
  people.resources :notes, :name_prefix => "person_"
end

map.resources :notes do |notes|
  notes.resources :comments, :name_prefix => "note_"
end

map.resources :comments

You’ll notice that I define each resource (except accounts) twice: once at the top level, and once nested within another resource. For the nested resources, I also give a “name_prefix”—this gets tacked onto the front of the named routes that are generated.

So, the above mappings give you the following named routes:

1
2
3
4
5
6
7
8
accounts_url          #-> /accounts
account_url(1)        #-> /accounts/1
account_people_url(1) #-> /accounts/1/people
person_url(2)         #-> /people/2
person_notes_url(2)   #-> /people/2/notes
note_url(3)           #-> /notes/3
note_comments_url(3)  #-> /notes/3/comments
comment_url(4)        #-> /comments/4

The URL’s are shorter, and the parameters to the named routes are much simpler. It’s an all-around win! I won’t go so far as to say that resources should never be deeply nested, but I will say that you should think long and hard before you go that route.

Posted in Tips & Tricks

Comments

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

05 Feb 2007

1. Damien McKenna said...

How about e.g. page numbers for portions of content? e.g.: 2007 conference page 3 -> /conference/2007/page/3 Would (manual or automatic) pagination be considered a special case?

2. Jamis said...

Damien, I don’t think pagination fits in routing. Personally, I prefer using the query string for that: ”/conference/2007?page=3”. The “third page of the conference resource” is not itself a resource, IMO. It’s a slice of the view of that resource, and using the query string makes it more apparent that you’re simply filtering the view.

3. Ben Askins said...

You’re right Jamis, once you start deeply nesting resources you’re creating a rod for your own back. The convenience of being able to reference comment_url(4) far outweighs the ‘purity’ of always having the refer to a comment within the scope of it’s parent resource.

4. Tom said...

Thanks for this post. Good advice.

As an aside, why is there never any interest in accessing nested resources by an index into the collection rather than the resource’s global id? For example, wouldn’t it be nice to have /people/2/notes/5 go to the fifth note for person 2, rather than simply to Note.find(5) (which, of course, might not even belong to Person.find(2))?

It’s not like it’s tricky to arrange, but I’m still always surprised that it’s never suggested or explored in articles like this one.

5. Thijs van der Vossen said...

We’ve invented a second rule of thumb; resources should only be nested for actions that really need the parent id (like ‘index’, ‘new’, and ‘create’).

We started using this for an application with a data model that’s almost entirely hierarchical except where it’s not and it seems to work very nicely. Why keep the parent resource in the url when the structure is not strictly hierarchical?

6. Tammer Saleh said...

We have an application that actually needs deeply nested resources for security reasons. We ended up adding helpers for each resource that climbs up the hierarchy to determine what the url should be. I don’t actually agree that deeply nested resources create overly complicated urls, but these helpers were definitely, um, helpful in keeping the code cleaner.

7. Jeff Coleman said...

Jamis,

If you created a route such as person_url(1,2), how would those be represented inside the controller? With a single ID, I know you’d reference params[:id], but how would you get the second ID?

Thanks!

8. Eric Mill said...

I definitely agree about complicated URLs. I believe that friendly, understandable URLs are important in keeping the web as unintimidating as possible to the layman. It’s part of the inclusive culture we should be setting on the Internet. So, though I agree with this from the perspective of making it an easier application to maintain as a developer, I also agree from an entirely different perspective, of making the application less frightening for my grandmother.

9. ed said...

Tom: The way I verify that (in your example) the Note belongs to Person.find(2) is put a before_filter that sets @person. So then in the #show method you do something like:

@note = @person.notes.find(params[:id])

...thus scoping the Notes to that Person.

10. Michael Raidel said...

@Jeff: in the case of person_url(1,2) you would reference the first parameter with params[:account_id], the second with params[:id]

11. ian said...

Thanks for clearing this up. Besides pretty URLs, I could never understand why you would want to nest resources so much.

Also, it gives you the illusion that your resources are not uniquely identifiable. Using your example, /accounts/1/people/2/notes would perform exact same as /accounts/99/people/2/notes since you are most likely just going to be looking at the person_id. The account_id can and probably will be derived from the person_id.

12. Jeff Coleman said...

Thanks, Michael!

13. Adam Cooper said...

In your final case with the comment resource. What happens in this case

note_comments_url(3) #-> /notes/3/comments comment_url(4) #-> /comments/4

Do these go to the same controller and if so how is this handled in the controller? Do you have extra logic to detect a param[:id] and ‘know’ that it represents the note object id?

14. Aaron Schaefer said...

Also, what happens when someone goes to just plain /people? Won’t it try to load the index method, but there would be no way to have an account associated with the call, and thus no way to determine which people to show? How can you differentiate the index function for /accounts/1/people and just plain /people in the PersonController?

06 Feb 2007

15. Lee Hericks said...

Adam and Aaron,

I believe you are getting into why DHH said this will help you make better controllers.

I am working on a e-book for modelling REST and resources in Rails. Right off the bat, you should be thinking about your use cases and the flow of your application.

Jamis, didn’t you blog something about starting with a user interface first?

In REST, you provide consistent URIs to your resources. A RESTful request to the server includes all the information it needs to dish out your resource, whatever it may be. In this case, a new comment needs the note_id that it belongs to. You can include the id in your POST to create the comment. Rails’ notion of a resource just puts the id in the URL and extracts it for you. Think of POST /notes/1/comments as convenient and only used because you were given a named_route that made it easy for you to use. As I mentioned, normally you might be including all the important information in your POST.

Which leads to how did your client get that id? In the case of a Rails application, it may be because the client pulled up an account, then the account’s user, saw a user note and went to comment on it. The state of your application transfered from page to page until you accomplished your goal (very RESTful). Therefore, you never needed to expose a /comments resource because your application didn’t need it.

You really need to consider how someone will access your application. Maybe it’s all running through XML and your client is caching the ids and can include them in the POST. As long as you receive them in a consistent manner (same name in your params) you can use the same controller. For comments, it makes absolutely no sense to retrieve a list of all comments unbound from their associated notes. If you feel you need to make it accessible, you should provide the URI to the note with each comment to make it RESTful. (So a client can access the notes and continue to crawl along the data)

In the situation of people, again you have to examine why the client is requesting that resource (if necessary) and what it should return. With GET /people you are requesting a list of all people resources. This list is not associated with an account. People are simply a resource to use if your client is entering your resources at that point rather than entering at the account point. If you expose a resource, you better have a reason for accessing it. Maybe an admin part of the application deals with people. Maybe people can be a part of many accounts. So you want to access people first, then list their accounts. Now, do you really want to muck around with conditionals and test for an account_id and dish out different information from the same action? No. Remember my reference to DHH’s keynote. Maybe you want both functionalities, but maybe they don’t belong together.

map.resources :people # PersonController

map.resources :accounts do |account| account.resources :people, :controller => “accounts_people” end

(Note: account_people may not be correct, but you want to get to the AccountPersonController. Basically a controller to that join relationship)

Two controllers for two different logical resources. Sure, they access the same ActiveRecords, BUT they have a different purpose. They are essentially a different resource for a client to use, and the controllers will be much cleaner and use-specific. I have been questioning why nested resources don’t point to a joined name controller. This is one of the issues I want to address in my book.

Jamis, if you are interested and have time, I’d love to talk with you more about resources and discovering how to get people into a new mindset of application design and developing with resources. I don’t think it’s appropriate to view resources as a direct proxy to an ActiveRecord class. Unfortunately, I think some people may think that way when first learning about REST and resources. This is definitely a new area to explore in Rails and it is different from normal MVC.

16. Eric said...

Hi Jamis,

I’m happy to wake up everyday, simply thinking that there might be a new great tip on this blog!

I work on a web-app displaying annual, monthly or daily reports for a given weatherstation, and I ended up using this piece of code in my routes.rb :

map.with_options :controller => 'display', :id => /[0-9]+(\-[a-z_]+)?/ do |report| report.annual 'display/:id/:year', :action => 'annual_report', :year => /(19|20)\d\d/ report.monthly 'display/:id/:year/:month', :action => 'monthly_report', :year => /(19|20)\d\d/, :month => /[01]?\d/ report.daily 'display/:id/:year/:month/:day', :action => 'daily_report', :year => /(19|20)\d\d/, :month => /[01]?\d/, :day => /[0-3]?\d/ end

Do you see possible improvement?

thanks again,

Eric

07 Feb 2007

17. Aaron Wheeler said...

I agree that simple URLs are nice. But what about the example where you want to be able to display continents, countries, and states? So this is an example with three levels of nesting.

Without nesting routes more than one deep, the route for countries would be declared twice – once nested under continents, and once with states nested in it. But what if we don’t want countries existing as a top level route? What if we prefer it to be nested?

While I haven’t dug into the source to confirm this, my testing has shown that nesting resources is a glorified way automatically setting the :path_prefix option. So an alternative to what Jamis suggests is to keep your routes nested, but change the :path_prefix, like so:

map.resources :continents do |continents| continents.resources :countries do |countries| countries.resources :states, :path_prefix => ’/countries/:country_id’ end end

The implicit :path_prefix for states is /continents/:continent_id/countries/:country_id, but this is overwritten by the explicit :path_prefix above.

This way your routes are still easy to read, but the URLs will print with only one level of nesting.

18. Jamis said...

Aaron, note that the reason I declare some of the routes twice is in order to gain the named routes I want. I want (using the example in the article) both note_url and people_notes_url, and you can’t get that with a single map.resource call.

In your countries example, the key line is “What if we prefer it to be nested?” Then, by all means, nest them! :) Do what makes you happy. Long, deeply nested routes make me sad, personally, so I avoid them using the tip posted here.

08 Feb 2007

19. Denis J. Cirulis said...

Jamis, I’ve tried to refactor my routes in my latest crm project :) I had 4 or even 5 level nesting. Now it works like a charm, I mean urls are shorter and parameter counts are shorter. Very good article !

14 Feb 2007

20. Adam said...

One problem I see with this is that in most cases if you remove the nesting, the URLs are no longer traversable. When I see /notes/3, I assume /notes will list all of the notes I want to see. However, in this case I really want /people/2/notes. If the URL for the note was /people/2/notes/3 then the context I need would be already in the URL.

Several browsers, such as Safari (cmd+click title bar), have this type of directory traversal as a built in feature. Not to mention that it’s quite common for users to edit the URL by hand to achieve the same effect. So, I believe it is important to keep the hierarchy intact whenever possible for the usability aspect, if nothing else. Unless the situation says that requesting /notes is preferred to /people/2/notes, the nesting should remain.

I do agree that person_path(a,b,c,d) is quite ugly, but there is no reason why calling person_path(d) can’t resolve a, b, and c automatically.

16 Feb 2007

21. Mike Schwab said...

It seems to me that the key to great resources is name_prefix. This way the nest coexists with the pure resources, and you get the best tool for every situation. In a crowded domain, I think it can be important to scope a collection by two of its peers, or even three. When the user gets back that long address, at least it explains just what they’re looking at (as well as how they can bookmark it or link others to just that info).

24 Feb 2007

22. WOL said...

What happens with things like edit_xyz_path?