34

I have a pretty common case for nested routes, I feel like, that looks something like this (in some sort of pseudonotation):

'/:username/photos' => Show photos for User.find_by_username
'/photos' => Show photos for User.all

In a nutshell: I have users. They have photos. I want to be able to show their photos on their page. I also want to be able to show all photos, regardless of the user. I'd like to keep my routes RESTful and using the built-in resource methods feels like the right way to do it.


Option 1 for doing this is to have PhotosController#index use a conditional to check which params are given and get the list of photos and set the view (different for a user's photos than for all photos). It's even easy to route it:

resources :photos, :only => [:index]
scope ':/username' do
  resources :photos
end

Boom. It'd seem like Rails was setup for this. After the routes, though, things get more complicated. That conditional back in the PhotosController#index action is just getting more and more bloated and is doing an awful lot of delgation. As the application grows and so do the number of ways I want to show photos, it is only going to get worse.

Option 2 might be to have a User::PhotosController to handle user photos, and a PhotosController to handle showing all photos.

resources :photos, :only => [:index]
namespace :user, :path => '/:username' do
  resources :photos
end

That generates the following routes:

           photos GET    /photos(.:format)                    {:action=>"index", :controller=>"photos"}
      user_photos GET    /:username/photos(.:format)          {:action=>"index", :controller=>"user/photos"}
                  POST   /:username/photos(.:format)          {:action=>"create", :controller=>"user/photos"}
   new_user_photo GET    /:username/photos/new(.:format)      {:action=>"new", :controller=>"user/photos"}
  edit_user_photo GET    /:username/photos/:id/edit(.:format) {:action=>"edit", :controller=>"user/photos"}
       user_photo GET    /:username/photos/:id(.:format)      {:action=>"show", :controller=>"user/photos"}
                  PUT    /:username/photos/:id(.:format)      {:action=>"update", :controller=>"user/photos"}
                  DELETE /:username/photos/:id(.:format)      {:action=>"destroy", :controller=>"user/photos"}

This works pretty well, I think, but everything is under a User module and I feel like that might end up causing problems when I integrate it with other things.

Questions

  • Does anybody have experience with something like this?
  • Can anybody share a better way of handling this?
  • Any additional pros and cons to consider with either of these options?

Update: I've gone ahead implementing Option 2 because it feels cleaner allowing Rails' logic to work rather than overriding it. So far things are going well, but I also needed to rename my namespace to :users and add an :as => :user to keep it from clashing with my User model. I've also overridden the to_param method on the User model to return the username. Path helpers still work this way, too.

I'd still appreciate feedback on this method. Am I doing things the expected way, or am I misusing this functionality?

2
  • 1
    You can ask whatever you want of course, but you contradict yourself a bit by asking for RESTful routes and explicitly demanding routes like /:username/photos which is surely not RESTful. I think the question is valid even for really RESTful routes like /users/:user_id/photos and if the solution is found for this case than it is another question how to tweak routes to map to if the way you need (in case you really need it, which I do not think is a good idea whatif some :username is photos ;-) then there is a conflict in routes etc...) Commented Feb 24, 2011 at 11:55
  • 4
    @gom Being RESTful has nothing to do with the actual URL segments. It has to do with utilizing HTTP request verbs (GET, POST, PUT, DELETE) to determine the appropriate action. As for vanity URLs (like /:username), this is really common. While it's been debated ad nauseam here, it's not the part causing problems. You can easily validate that a username is not in a list of reserved names, and because my routes are setup properly, it can't override any of my other routes. Commented Feb 24, 2011 at 15:45

5 Answers 5

7

Have you considered using a shallow nested route in this case?

Shallow Route Nesting At times, nested resources can produce cumbersome URLs. A solution to this is to use shallow route nesting:

resources :products, :shallow => true do
  resources :reviews
end

This will enable the recognition of the following routes:

/products/1 => product_path(1)
/products/1/reviews => product_reviews_index_path(1)
/reviews/2 => reviews_path(2)
Sign up to request clarification or add additional context in comments.

3 Comments

This doesn't meet the routing constraint of having the username as the first URL segment, but that's alright because I can address that. However, this doesn't really help answer my questions about how to handle the actions conditionally in a clean way.
Username are a dangerous first url segment...it's canonically part of a query string, but I suppose you can check to make sure you don't have any users named photos etc. As far as conditionals go, I would add separate urls for those, if they can't easily be expressed by query strings. This moves your bloated conditional to the routing logic. It has to go one place or another.
Thanks for the warning, but I've been using them in non-Rails apps for years sans problem. It's not really acceptable to not do vanity URLs; even Facebook caved into that demand.
6

The best way to do this depends on the application, but in my case it is certainly Option B. Using namespaced routes I'm able to use a module to keep different concerns separated out into different controllers in a very clean way. I'm also using a namespace-specific controller to add shared functionality to all controllers in a particular namespace (adding, for example, a before_filter to check for authentication and permission for all resources in the namespace).

1 Comment

it would be nice if you could provide a code example of this! (even if it's just copying and pasting above option and adding some example controller code as well)
2

I did something similar to this in one of my apps. You're on the right track. What I did was declare nested resources, and build the query using the flexible arel-based syntax of Active Record in Rails 3. In your case it might look something like this:

# config/routes.rb
resources :photos, :only => :index
resources :users do
  resources :photos
end

# app/controllers/photos_controller.rb
def index
  @photos = Photo.scoped
  @photos = @photos.by_user(params[:user_id]) if params[:user_id]
  # ...
end

4 Comments

That doesn't route the way I've described above (e.g. '/username/photos'). What about toggling the view?
Well, if you literally wanted their username in the URL, you could override to_param on the User model. If it's also important for the route to begin with the username (as opposed to /users/:username), search around, as that question has already been asked several times on SO. Personally I don't think it's a good approach as it sets you up for potential naming conflicts with future controllers. It's easy to do something different in the view – you can write conditional logic dependent on the presence of params[:user_id].
Overriding to_param seems like it might be a decent option to make path helpers work without passing the username in explicitly. Short of that, though, I think I like the clarity that "Option 2" offers; Rails already knows to load the photos differently, and the right view is loaded every time. Regarding naming conflicts on the vanity urls, I've done it in the past without issues. As long as you put it last and constrain the routes to valid users it works well. In this case I have the "/photos/*" to keep wild URLs from being routed as "/username/foobar", too.
Jimmy: I think that the question was asked, just because "That conditional back in the PhotosController#index action is just getting more and more bloated and is doing an awful lot of delgation." So yes it is easy to write conditional logic, but it is goibg to be bloated. coreyward: What exactly do you mean by "Rails already knows to load the photos differently". You still need to do it by yourself, don't you?
0

You could define a seperate route for getting the photos for one user like so:

get '(:username)/photos', :to => 'photos#index'

But I would advise just using the nested resource that Jimmy posted above since that is the most flexible solution.

1 Comment

The route from Jimmy Cuadra doesn't fit my needs. I don't see how it's more flexible than either of the options I posted. The route you suggest is just defining one of the many routes I already have in Option 1.
0
Example::Application.routes.draw do
  resources :accounts, :path => '' do
    resources :projects, :path => '', :except => [:index]
  end
end

Got the example from: http://jasoncodes.com/posts/rails-3-nested-resource-slugs

Just applied that in my current project.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.