Extending ActionController::Base
once, in ApplicationController
, is a great Ruby on Rails practice. However, if ApplicationController
is your only abstract controller, it’s likely to become a maintenance challenge. To avoid this, you should extend ApplicationController
as needed and move as much code as possible into its subclasses.
Feeling ApplicationController Pain
So, our app is live. We’ve dutily extended ApplicationController
in all our other controllers, giving us an inheritance tree like this:
Fortunately, our app is a success and our customers want us to open an API. Let’s use API::BaseController
as the superclass of all our API controllers:
As our user base grows, we need a more robust permissions system. We tighten up restrictions:
class ApplicationController < ActionController::Base
before_action :authenticate!
end
Since some actions are public, we skip the restrictions:
class ReportsController < ApplicationController
skip_before_action :authenticate!
end
# ...
class ProfilesController < ApplicationController
skip_before_action :authenticate!
end
We’ve forgotten about our API, and when we deploy, we’ll be quickly reminded that ApplicationController
is involved in those requests too. Since ApplicationController
touches every request, it’s hard to be sure about exactly what will be affected by changes there.
ApplicationController Gains Weight
Left alone, ApplicationController
can bloat for many reasons:
- Authentication logic, perhaps with complex branching based on what the user is accessing, builds up little-by-little as the application is extended.
- Before-actions & helpers which are used often but not always tend to accrue in
ApplicationController
, since they’re “used more than once.” - Oddball routes might be implemented in
ApplicationController
because no other existing controller seems like the right place.
In JavaScript development, filling the global namespace with application code is a no-no. Similarly, ApplicationController
is a near-global namespace, so each addition to it should be considered very carefully. When we add to (and remove from) ApplicationController
, we’re potentially altering every request that our application serves; how can we be sure we aren’t breaking something?
Isolating “Parts” of the App
Returning to the example above, I think this inheritance tree is better:
We’ve introduced abstract classes for each “part” of the app. (I use quotes because I don’t know a technical term for it!) Now, logged-in authentication would be handled by a subclass of ApplicationController
, perhaps named BaseController
. A logged-in controller would extend BaseController
. For example:
class ItemsController < BaseController
# ...
end
Similarly, public controllers would be in a namespace of their own, with their own base controller. For example:
class Public::ProfilesController < Public::BaseController
# ...
end
This is good because:
- You can refactor with more confidence, since you only have to load part of the app into memory when working on abstract controllers.
- Stable parts of the app are more likely to remain stable (since they won’t be affected by other parts).
The corresponding file structure looks like this:
controllers/
api/
base_controller.rb
items_controller.rb
profiles_controller.rb
public/
base_controller.rb
reports_controller.rb
profiles_controller.rb
staff/
base_controller.rb
stats_controller.rb
application_controller.rb
base_controller.rb
items_controller.rb
profiles_controller.rb
reports_controller.rb
(I’ve left some controllers in the root namespace. If you like, you could put logged-in actions in a namespace too!)
And the routes might look like this:
namespace :api do
resources :items, :profiles
end
namespace :public do
resources :profiles, :reports
end
namespace :staff do
resources :stats
end
resources :items, :profiles, :reports
When should we extend ApplicationController?
I’d say it’s good to extend ApplicationController
for each “part” of the app. It’s a bit subjective, but here are some clues:
- Actions rendered with a different layout (or lack thereof). Your webservice, administration and public views are distinct parts of your app.
- Actions using different authentication strategies. Keep API endpoints, public pages, and staff-only actions in separate sections. If a staff member goes rogue, you’ll be able to tighten up that part of the app confidently :)
- Actions with different frequently-used helpers or before-actions. If there’s a before-action that’s often skipped, Rails wants to tell you something: these controllers are different! Similarly, if you have controller-level helper methods, perhaps the controllers who depend on that helper should be in a “part” of their own.
I hope a pattern like this will give you more freedom & confidence when refactoring important parts of the request-response cycle!