Namespaces in Rails Applications
In general, Rails doesn’t encourage developers to factor code into namespaces. Most apps end up with hundreds of files and very few directories in app/models
and similarly flat hierarchies in other app
directories. Rails Engines are a great way to pull your code into isolated, reusable modules, but there’s a significant amount of overhead associated with an Engine that might be too expensive for a small feature, or you might just have a few conceptually similar models that you want to “live near each other”.
Rails has support for namespacing in its MVC architecture, but there are a few things that might cause confusion if you haven’t used it before.
Pretend your app has a user-configurable dashboard. Users can add widgets — like charts and tables — to the dashboard, and they can lay the widgets out on the dashboard in rows and columns. You might model it like this:
Dashboard::Dashboard
, which containsRows
Dashboard::Row
, which containsWidgets
Dashboard::Widget
, which has a position within itsRow
I’ll use these models in the examples below.
Models
Most of the gnarly bits of namespacing happen in the models, so I’ll start there.
First, you’ll need to create a module for your namespace and define the class method table_name_prefix
(see Migrations):
# app/models/dashboard.rb
module Dashboard
def self.table_name_prefix
'dashboard_'
end
end
Once you’re “inside” the namespace, you generally won’t refer to it when declaring associations. For example, Widget
will belong to Row
and Row
will belong to Dashboard
:
# app/models/dashboard/widget.rb
module Dashboard
class Widget < ApplicationRecord
belongs_to :row
end
end
# app/models/dashboard/row.rb
module Dashboard
class Row < ApplicationRecord
belongs_to :dashboard
has_many :widgets
end
end
# app/models/dashboard/dashboard.rb
module Dashboard
class Dashboard < ApplicationRecord
has_many :rows
end
end
Outside the namespace, though, you’ll have to tell Rails the class name of any association that uses your namespaced model. Otherwise, it will guess incorrectly:
# app/models/user.rb
class User < ApplicationRecord
# WRONG => NameError: uninitialized constant User::DashboardDashboard
has_many :dashboard_dashboards
# RIGHT
has_many :dashboard_dashboards, class_name: 'Dashboard::Dashboard'
# ALSO RIGHT
has_many :dashboards, class_name: 'Dashboard::Dashboard'
end
Migrations
Since most databases don’t support namespaces for tables, Rails expects you to prefix your table names with the “underscored” name of your namespace. You also won’t be able to pass foreign_key: true
to the references
helper method, so you’ll have to create constraints yourself.
To create the migration for Dashboard::Widget
:
$ rails generate migration create_dashboard_widgets
Edit the generated file:
# db/migrate/YYYYMMDDHHMMSS_create_dashboard_widgets.rb
class CreateDashboardWidgets < ActiveRecord::Migration
create_table :dashboard_widgets do |t|
# adding "foreign_key: true" here will cause an error like this:
# PG::UndefinedTable: ERROR: relation "rows" does not exist
t.references :row, index: true
end
add_foreign_key :dashboard_widgets, :dashboard_rows, column: :row_id
end
Routes
Simply wrap your routes in a call to namespace
:
# config/routes.rb
namespace :dashboard do
resources :dashboards
# ...
end
Your URLs will look like /dashboard/dashboards/1
, and path helpers will look like edit_dashboard_dashboard_path(dashboard)
.
Controllers
Controllers are also straightforward:
# app/controllers/dashboard/dashboards_controller.rb
module Dashboard
class DashboardsController < ApplicationController
def index
@dashboards = current_user.dashboards
end
# ...
end
end
Views
You may get a confusing error if you try to render a partial of a model from outside the namespace.
Say one of your widgets renders a list of recently active users:
# app/views/dashboard/widgets/_active_user_widget.html.erb
<%# This can result in an error:
Missing partial dashboard/users/_user %>
<%= render @widget.active_users %>
Rails is attempting to render a different partial than you might expect since you’re inside a namespace. The idea is that you might want a different _user
partial in, say, an admin area than you do in the public area.
Since I’m generally using namespaces to isolate features rather than create distinct layouts, I turn this behavior off:
# config/application.rb
config.action_view.prefix_partial_path_with_controller_namespace = false
Unfortunately, it’s a global setting, so you can’t change it for each call to render
. You can always explicitly render a template if you need the default behavior:
<%= render partial: 'admin/users/user', collection: @users %>
Factories
If you’re using FactoryGirl for testing, you’ll need to tell her the name of the factory to use for your namespaced associations:
# spec/factories/dashboard/widget.rb
FactoryGirl.define do
factory :dashboard_widget, class: Dashboard::Widget do
association :row, factory: :dashboard_row
end
end
End
After many years of cramming everything directly into app/models
, I’ve had great luck with namespacing. It keeps related concepts together and provides a nice level of isolation without the overhead of full-fledged Engines or microservices.
I hope your experience is as pleasant as mine!