Routing nested resources in Rails 3
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
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, 2022Comments
-
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 myUser
model. I've also overridden theto_param
method on theUser
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 about 13 yearsThat doesn't route the way I've described above (e.g. '/username/photos'). What about toggling the view?
-
Jimmy about 13 yearsWell, 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 ofparams[:user_id]
. -
coreyward about 13 yearsOverriding
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 about 13 yearsThe 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 about 13 yearsThis 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 about 13 yearsUsername 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 about 13 yearsJimmy: 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 about 13 yearsThanks 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 over 12 yearsit 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)