33
votes

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?

5
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...) - gorn
@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. - coreyward

5 Answers

7
votes

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)
6
votes

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).

2
votes

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
0
votes

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.

0
votes
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.