Posted almost 5 years ago. Visible to the public.

Rails routes: Extracting collection actions into their own controllers

Let's say you have two screens:

  1. Show a given project
  2. Show a report for all projects

Ideally you want both screens to be handled by different controllers like this:

GET /projects/:id => ProjectsController#show GET /projects/report => Projects::ReportsController#show

What seems like a simple requirement is a little awkward to configure in your routes.
Obviously the report should be a singleton resource, but how can we nest it into the Projects:: namespace?

What does not work is this:

resources :projects, only: :show do resource :report, only: :show end

If we defined the routes above we would get a report for each project, instead of one report across all projects:

GET /projects/:id => ProjectsController#show GET /projects/:id/report => Projects::ReportsController#show

Hacks to solve this include renaming the report route to something like /projects_report, or defining
a new collection action ProjectsController#report. But these are all unsatisfying.

What you can do is wrap the sub-resource in a collection block like you would do with custom collection actions:

resources :projects, only: :show do collection do resource :report, only: :show, controller: 'projects/report' resources :members, only: [:index, :show] end end

Run rake routes and you get what you wanted in the first place:

GET /projects/:id => ProjectsController#show GET /projects/report => Projects::ReportsController#show GET /projects/members => MembersController#index GET /projects/members/:id => MembersController#show

Note how /projects/report, /projects/members and /projects/members/:id do not take a :project_id.


Take care when defining nested resources, as they don't behave identical to namespaced resources:

namespace :users do resources :sign_ups, only: [:new] end # GET /users/sign_ups/new => Users::SignUpsController#new
resources :users, only: [] do resources :sign_ups, only: [:new] end # GET /users/sign_ups/new => SignUpsController#new

Both solutions will generate the same routes, the controller paths will be different, though. In the first example rails will expect to find a Users::SignUpsController in the subdirectory controllers/users/sign_ups_controller.rb, but there has to be a SignUpsController in controllers/sign_ups_controller.rb in order to make the second example work.
You can alter this behaviour with a custom controller path, as shown in the very first example of this card (resource :report, only: :show, controller: 'projects/report').

Solution for legacy Rails versions

What you can do instead is simple re-use the :projects symbol as both a resource and a namespace:

namespace :projects, as: :project do resource :report, only: :show end resources :projects, only: :show

Note that the routes must appear in this order so projects/report won't match the :projects resource.

Run rake routes and you get what you wanted in the first place:

GET /projects/report => Projects::ReportsController#show GET /projects/:id => ProjectsController#show

By refactoring problematic code and creating automated tests, makandra can vastly improve the maintainability of your Rails application.

Owner of this card:

Henning Koch
Last edit:
6 months ago
by Jakob Scholz
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Henning Koch to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more