Streamlining Rails controllers with custom filters

As Rails applications grow, managing subscription-based access can overload controllers with complex logic. This article explores how to use custom filter classes to extract subscription-related checks from controllers, resulting in cleaner, more maintainable code.

The Problem: Subscription Checks in Controllers

Consider a typical scenario where we need to check a user's subscription status before granting access to certain features:

class PremiumContentController < ApplicationController
  before_action :check_subscription

  def check_subscription
    user = current_user
    unless user&.active_subscription? && !user.subscription_expired?
      flash[:error] = generate_subscription_error(user)
      redirect_to upgrade_path
    end
  end

  def generate_subscription_error(user)
    return "You must be logged in" if user.nil?
    return "Your subscription has expired" if user.subscription_expired?
    return "You need an active subscription" unless user.active_subscription?
  end

  # ... other controller actions for premium content
end

This approach works, but mixes subscription logic with controller responsibilities, resulting in code that is harder to maintain.

Introducing custom subscription filters

Let's refactor this using a custom filter class to encapsulate the subscription-related logic:

# app/controllers/premium_content_controller.rb
class PremiumContentController < ApplicationController
  before_action SubscriptionFilter
end

# app/filters/subscription_filter.rb
class SubscriptionFilter
  def self.before(controller)
    user = controller.send(:current_user)
    
    unless user&.active_subscription? && !user.subscription_expired?
      controller.flash[:error] = subscription_error_message(user)
      controller.redirect_to controller.upgrade_path
    end
  end

  private

  def self.subscription_error_message(user)
    return "You must be logged in" if user.nil?
    return "Your subscription has expired" if user.subscription_expired?
    return "You need an active subscription" unless user.active_subscription?
  end
end

Let's analyse the key components of our custom subscription filter:

  • Class definition: We define SubscriptionFilter as a standalone class.
    self.before method: This class method is called by Rails before controller actions. It takes the controller instance as an argument, allowing access to controller methods and properties.
  • User call: We use controller.send(:current_user) to access the current_user method, which is typically private in controllers.
  • Subscription validation: The filter checks that the user has an active, non-expired subscription.
  • Error handling: If conditions aren't met, we set a flash error message and redirect the user to an upgrade path.
  • Private helper method: subscription_error_message generates appropriate error messages based on the user's subscription status.

Benefits of custom subscription filters

Doing filters via this approach we could achieve:

  • Focused controllers: Controllers stay lean and focused on their primary responsibilities.
  • Reusability: The subscription filter can be applied to multiple controllers that require subscription checks.
  • Easier testing: Subscription logic can be unit-tested independent of controllers.
    Flexibility: Subscription rules can be changed in one place without touching multiple controllers.

Implementation Tips

Try to store custom filter classes in app/filters/ for organization and use descriptive names for your filter classes (e.g. TrialPeriodFilter, PremiumTierFilter). When possible, consider creating a base filter class for common subscription-related functionality(in that example case).
You might think that we could use helpers or modules for that - and yes, that will work, but having separate filters for controllers I think is much more elegant.

Expand the concept

You can create several subscription-related filters for different scenarios:

class TrialPeriodFilter
  def self.before(controller)
    user = controller.send(:current_user)
    if user&.trial_expired?
      controller.flash[:notice] = "Your trial has ended. Please upgrade to continue."
      controller.redirect_to controller.pricing_path
    end
  end
end

class PremiumContentController < ApplicationController
  before_action SubscriptionFilter
  before_action TrialPeriodFilter, only: [:exclusive_content]
end

This approach allows granular control over different subscription states and access to content.

Summary

Custom subscription filters in Rails provide a powerful way to manage subscription-based access control while keeping controllers clean and focused. By adopting this approach, you can create more maintainable, modular and testable Rails applications, especially when dealing with complex subscription logic.

Happy filtering!