In our previous post about using Ruby on Rails Model Concerns, we explored how using concerns in our Rails projects help keep our codebase models clean and maintainable. Today, we're diving into Controller Concerns - another powerful feature we regularly use at Add Jam to organise our Rails applications.
Why Use Controller Concerns?
We've found that in MVC applications the controllers can quickly become cluttered with logic or we have some core repeatable functionality that we want to use across multiple controllers. With the mantra of keeping our code DRY we utilise Controller Concerns to help us extract common patterns and logic into reusable modules, making our controllers cleaner, more focused on their primary responsibilities and easier to test.
Let's explore some real-world Controller Concerns we've implemented across our Ruby on Rails web projects.
1. Authentication Concern
Authentication logic often spreads across multiple controllers. By extracting it into a concern, we can maintain consistent authentication behaviour throughout our application:
module Authenticatable
extend ActiveSupport::Concern
included do
before_action :authenticate_user!
helper_method :current_user
private
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
def authenticate_user!
login_user!
unless current_user.present?
redirect_to login_path, alert: "Please sign in to continue"
end
end
end
end
This is a really light touch authentication concern example (and importantly assumes you have some kind of actual authentication system in place - ** this is not a comprehensive example**). But here we have a few convenance methods we can use across our controllers simply by including it in your application controller:
class ApplicationController < ActionController::Base
include Authenticatable
end
And assuming your domain specific controllers in your app are inheriting from ApplicationController we now have authentication logic available to us. Simple.
All the authentication logic can be located in one place making it DRY and therefore more maintainable and easier to test.
2. Using Header values
A common use we have for Controller Concerns is to access header values in a request and do something with it. For example, let's say we have a client that is sending a Version string as a header. Here's a real-world concern we use to access this value and do something with it.
module VersionHeader
extend ActiveSupport::Concern
included do
before_action :check_version
end
private
def check_version
version = request.get_header("HTTP_X_VERSION")
return if version.blank?
unless version_valid?(version)
render json: { error: "Invalid" }, status: 422
end
end
def version_valid?(version)
# Add your version validation logic here
end
end
The VersionHeader
concern is designed to automatically get the version header from a request and if the version is not valid we can render an error. Simple approach to API versioning that might come in handy with say a mobile app that where you want to stop older versions of the app from accessing the API.
Rather than pepper that logic through your controllers we can now just include this concern in our application controller and have it available to us.
3. Localisation Management
For modern web applications serving an international audience, handling localisation properly is essential. Rails has really good i18n support (perhaps that's a topic for another post) for managing the locationsation content but how can we set the locale for a request? Enter the Locale concern:
module Locale
extend ActiveSupport::Concern
included do
around_action :switch_locale
private
def switch_locale(&action)
locale = Current.user.try(:locale) || I18n.default_locale
I18n.with_locale(locale, &action)
end
end
end
This concern automatically sets the appropriate locale based on user preferences, ensuring consistent language handling throughout the request lifecycle.
Best Practices for Controller Concerns
Through our experience building Rails applications, we've developed these guidelines for Controller Concerns:
- Single Responsibility: Each concern should handle one specific aspect of controller functionality
- Keep Them Light: Concerns should contain minimal logic - consider 'services' for complex operations
- Consistent Naming: Use clear, descriptive names that indicate the concern's purpose
- Documentation: Include clear documentation explaining when and how to use the concern. Easy to be caught out by weird behaviour if you don't know what's going on in ApplicationController.
- Testing: Test, test, test. Write dedicated specs for your concerns to ensure reliability
Testing Controller Concerns
Here's how we test our Controller Concerns (like our Model Concerns post we're using RSpec):
RSpec.describe PushToken, type: :controller do
controller(ApplicationController) do
include PushToken
def index
render plain: "OK"
end
end
describe "#save_user_device" do
let(:user) { create(:user) }
before do
allow(Current).to receive(:user).and_return(user)
request.headers["HTTP_X_PUSH_TOKEN"] = "test-token"
end
it "creates a new device token" do
expect {
get :index
}.to change(user.device_tokens, :count).by(1)
end
it "updates last_used_at for existing tokens" do
token = create(:device_token, user: user, token: "test-token")
get :index
expect(token.reload.last_used_at).to be_present
end
end
end
RSpec.describe VersionHeader, type: :controller do
controller(ApplicationController) do
include VersionHeader
def index
render plain: "OK"
end
end
describe "#check_version" do
before do
allow(controller).to receive(:version_valid?).and_return(true)
end
context "when version header is present" do
before do
request.headers["HTTP_X_VERSION"] = "1.0"
end
it "calls version_valid? with the version" do
expect(controller).to receive(:version_valid?).with("1.0")
get :index
end
it "renders OK if version is valid" do
get :index
expect(response.body).to eq("OK")
end
end
context "when version header is invalid" do
before do
allow(controller).to receive(:version_valid?).and_return(false)
request.headers["HTTP_X_VERSION"] = "invalid"
end
it "renders an error if version is invalid" do
get :index
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include("Invalid version")
end
end
context "when version header is not present" do
it "does not call version_valid?" do
expect(controller).not_to receive(:version_valid?)
get :index
end
it "renders OK" do
get :index
expect(response.body).to eq("OK")
end
end
end
end
Common Controller Concern use cases
We've successfully implemented Controller Concerns for various features in projects including:
- Request tracking and analytics
- API version handling
- Error handling and reporting
- Rate limiting
- Response formatting
There's a lot of scope for using Controller Concerns to keep your controllers clean and maintainable.
When NOT to use Controller Concerns
While concerns are powerful, they're not always the right tool for the job. Here are some situations where you might want to consider alternatives:
- Complex Business Logic: If your concern is handling complex business rules, consider using a service object instead
- Database Operations: Heavy database operations should probably live in a model or service
- External API Calls: These are better suited to dedicated service objects
- View-specific Logic: This belongs in helpers or view components
Remember, concerns are meant to be lightweight modules that extend controller functionality, not a dumping ground for complex logic.
Debugging Tips
When working with concerns, here are some common debugging approaches we use:
- Check load order: Make sure concerns are being loaded in the correct order
- Monitor performance: Watch for N+1 queries or slow operations in your concerns, they'll impact all your controllers that include them
- Test in isolation: Use RSpec's
describe
blocks to test concern methods independently
Unsure on when to use Controller Concerns?
Based on our experience, Controller Concerns are ideal when:
- Multiple controllers share common functionality
- You need consistent behaviour across controllers
- The shared logic is relatively simple and focused
- You want to keep your controllers DRY and maintainable
There's no hard and fast rule on when to use Controller Concerns and the choice of when to use them will come from experience. As a top tip if you find yourself repeating the same logic in multiple controllers then it's probably a good candidate for a concern.
Try it out
Hopefully we've convinced you that Controller Concerns are a powerful tool for organising Rails applications. At Add Jam, we've used them successfully across numerous projects to keep our controllers clean and maintainable while ensuring consistent behaviour throughout our applications. As well as Model Concerns which we previously posted about.
If you're looking to improve your Rails application's architecture or need help implementing these patterns, our team of experienced Rails developers can help. We offer:
- Custom Ruby on Rails development
- Code reviews and architectural guidance
- Performance optimisation
- Technical consulting
Get in touch with our Rails development team to discuss how we can help improve your Rails application.
Add Jam is a leading Ruby on Rails development agency based in Glasgow, Scotland. We specialise in building robust, scalable applications using Ruby on Rails, React, and React Native for clients across the UK and globally.