Routing nested resources in Rails 3

23,581

Solution 1

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

Solution 2

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)

Solution 3

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
Share:
23,581
coreyward
Author by

coreyward

I currently work at Figma as a web developer. I work on our marketing website, our DesignSystems publication, and our Config user conference website. Outside of Figma I do freelance work, mostly involving Gatsby/React and Sanity.io. I used to do a lot of full-stack development using Ruby on Rails (and PHP before that), but these days I tend to do more Jamstack/front-end work.

Updated on February 15, 2022

Comments

  • coreyward
    coreyward about 2 years

    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?

  • coreyward
    coreyward about 13 years
    That doesn't route the way I've described above (e.g. '/username/photos'). What about toggling the view?
  • Jimmy
    Jimmy about 13 years
    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].
  • coreyward
    coreyward about 13 years
    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.
  • coreyward
    coreyward about 13 years
    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.
  • coreyward
    coreyward about 13 years
    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.
  • MattMcKnight
    MattMcKnight about 13 years
    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.
  • gorn
    gorn about 13 years
    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?
  • coreyward
    coreyward about 13 years
    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.
  • Jon Lemmon
    Jon Lemmon over 12 years
    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)