f you've created a large Rails project, you may be encountering an organizational issue. Models, controllers, and other files live in the same folder, and can be difficult to tell apart. There may even exist multiple types of seemingly equivalent classes (e.g. an event participant vs a business participant) that require different logic and cannot live in the same namespace. Namespacing allows MVC files to be grouped into subfolders, greatly simplifying organizational complexity.
What is Namespacing?
Before we get into implementation details, we need to first differentiate some related routing concepts/keywords: namespacing, nesting, and scoping.
|Namespace||Organize under a folder||/parent/child/*||parent/child#action|
|Nested||Logical children of resources||/parent/:parent_id/child/*||child#action|
Scoped and Namespaced routes are very similar,
scope allows greater customization.
The table shows default values, but scope can be customized to change URI pattern (
path), controller (
module), and path helper names (
Nested routes are a bit different. A nested route can be a member, a collection, or another resource. Only a collection does not require a specific parent ID in the URI route.
They are all closely related. Using these different routing keywords, you can create identical routes, helpers, and folder structure, they just provide different ways to get there.
Assume we have Event and User resources, where Users can be Participants or Workers in these events.
First off, we need to generate these resources:
rails g scaffold Event and
rails g scaffold User.
The scaffold command will create model, migration, controller, and views for the resources.
Next, create the namespaced participant controller:
rails g controller Events::Participants.
For convenience sake, it will be responsible for creating any event role and thus we will avoid creating a Worker controller.
Let's define a custom, namespaced route in
config/routes.rb to add or remove a participant from an event:
1 2 3 4 5 6 7 Rails.application.routes.draw do namespace :events do put :add_or_rm_role, to: 'participants#add_or_rm_role' end resources :events resources :users #Recommended to use Devise or another library instead of writing your own authentication system end
rake routes will show:
1 2 3 4 Prefix Verb URI Pattern Controller#Action events_add_or_rm_role PUT /events/add_or_rm_role(.:format) events/participants#add_or_rm_role edit_event GET /events/:id/edit(.:format) events#edit #<<<< event resource route #other event/user resource routes...
Notice the order of the routes.
If event has a string ID, and the custom route 'add_or_rm_role' occurs after
add_or_rm_role will be interpreted as an event ID.
Let's setup the back-end migrations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 create_table :events do |t| t.string :name, null: false end create_table :users do |t| t.string :name, null: false end create_table :event_workers do |t| t.belongs_to :user t.belongs_to :event t.string :job #add some custom attributes to this join table for fun end create_table :event_participants do |t| t.belongs_to :user t.belongs_to :event t.boolean :user_showed_up, default: false end
We want to specify the associations between these models. Since join tables have custom attributes, and there are multiple user-event associations, we will use has_many instead of has_many_through.
1 2 3 4 class Event < ApplicationRecord #filename: event.rb has_many :workers, dependent: :destroy has_many :participants, dependent: :destroy end
1 2 3 4 class User < ApplicationRecord #filename: user.rb has_many :workers, dependent: :destroy has_many :participants, dependent: :destroy end
The following models are event namespaced, and thus should go into the
1 2 3 4 class Event::Worker < ApplicationRecord #filename: event/worker.rb belongs_to :event belongs_to :user end
1 2 3 4 class Event::Participant < ApplicationRecord #filename: event/participant.rb belongs_to :event belongs_to :user end
That indicates to rails that this model is namespaced, and is similar to what must be done in the controllers.
user_controller.rb will be created, but the participant controller is much more interesting.
Create a new file,
This controller will implement our custom route.
The logic is a bit application-specific here. The important thing to note is that we are creating our own strong parameters, and that the route points to the correct action for this namespaced route.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Events::ParticipantsController < ApplicationController def add_or_rm_role #choose which model we want to add model = nil event = Event.find(event_role_params[:event_id]) case event_role_params[:role] when "worker" model = Event::Worker when "participant" model = Event::Participant end #add/remove the DB tuple mod_role(model, event_role_params[:is_add], event.id, current_user.id) #current_user must be defined via authentication application #redirect user and refresh the page redirect_to event, notice: 'Changes have been saved!' end private # Never trust parameters from the scary internet, only allow the white list through. def event_role_params params.permit(:role, :is_add, :event_id, :user_id) end def mod_role(model, is_add, evt_id, usr_id) if ActiveModel::Type::Boolean.new.cast(is_add) model.create(event_id: evt_id, user_id: usr_id) else model.find_by(event_id: evt_id, user_id: usr_id).destroy end end end
We need a way for the user to call our new action.
This is pretty simple using link_to.
Let's insert this link in our
1 2 3 4 5 6 7 8 9 10 11 <% current_user_is_participating = @event.particpants.find_by(user_id: current_user.id) != nil if current_user_is_participating %%> <%= link_to "Remove myself from event", events_add_or_rm_role_path(role: Event::Participant.event_role_types[:participant], is_add: false, event_id: @event.id), method: :put %%> <% else %%> <%= link_to "Sign Up For Event", events_add_or_rm_role_path(role: Event::Participant.event_role_types[:participant], is_add: true, event_id: @event.id), method: :put %%> <% end %%>
events_add_or_rm_role_path is the helper from our Routing section.
The custom params defined in the controller are arguments to this helper function.
@event will be defined in the events#show action, and
current_user need to be defined in your authentication schema.
If you want link_to to use an ajax command, remove the
redirect_to in the controller action and add
remote: true to the link_to's.
We also need to display all of our participating users on the event#show page
1 2 3 4 <p>Participants:</p> <% for participant in @event.participants %%> <%= participant.user.attributes %%> <% end %%>
Sometimes namespacing can cause issues.
When setting this up, I kept running into the error:
NameError (uninitialized constant Participant).
I tracked it down to the CanCanCan authorization gem's
This call was not smart enough to see the model had been namespaced, and needed to know about it explicitly:
load_and_authorize_resource class: "Event::Participant", I imagine there are other gems/scenarios out there that act the same.
There are other ways this specific scenario could have been setup. Scope is an alternative to the namespace routing, and is a bit cleaner:
1 2 3 4 5 resources :events do scope module: :events do put :add_or_rm_role, to: 'participants#add_or_rm_role' end end
rake routes would resolve to
1 2 Prefix Verb URI Pattern Controller#Action event_add_or_rm_role PUT /events/:event_id/add_or_rm_role(.:format) events/participants#add_or_rm_role
Since this action is role agnostic, maybe a nested event route would be better.
1 2 3 4 5 resources :events do member do put :add_or_rm_role end end
rake routes now points to the events controller instead of the namespaced participants controller.
This prevents mucking around in namespaces, or even creating a new controller, but it can quickly cause the events controller to blow up.
1 2 Prefix Verb URI Pattern Controller#Action add_or_rm_role_event PUT /events/:id/add_or_rm_role(.:format) events#add_or_rm_role
From there, you'd have to add the action to the events controller, modify the strong parameters, and change the path helper in the link_to functions. The last option is probably the easiest to read and understand, but then I wouldn't have been able to write about namespacing!