Cleaner Rails Controllers with before_action

Cleaner Rails Controllers with before_action

If you have two or more render statements in your controller action, you might consider extracting some of the logic into a before action. Why? Because rendering or redirecting in a before_action will halt the request cycle and remove the need for an and return in your controller action. This can also help keep your controller actions focused, smaller, and easier to reason about for other developers.

Let’s look at an example, where we return early in an action if a user is suspended:

class OrdersController < ApplicationController
  def index
    if current_user.suspended?
      render json: { error: 'Your account is suspended.' }, status: :forbidden and return
    end

    @orders = current_user.orders
    render :index
  end
end

We could write this as this instead:

class OrdersController < ApplicationController
  before_action :verify_active_user, only: :index

  # Keeps our index method targeted and focused.
  def index
    @orders = current_user.orders
    render :index
  end

  private

  def verify_active_user
    if current_user.suspended?
      # No return necessary now, the request cycle will halt if you render/redirect in
      # before_action.
      render json: { error: 'Your account is suspended.' }, status: :forbidden
    end
  end
end
  • The index action is now focused only on its core responsibility—retrieving and rendering orders.

  • No need for and return, since render automatically halts execution.

Essentially we’ve simply performed the Extract Method refactoring, but with the added benefit of terminating the request early if our condition isn’t met.

A great side effect is that you can extract this logic into a concern and easily reuse it across a number of controllers:

class OrdersController < ApplicationController
  include SuspendedUserProtection

  def index
    @orders = current_user.orders
    render :index
  end
end

# This is an easy approach that could be used when the action needs to be applied
# very liberally without many exceptions (where you could use `skip_before_action`)
module SuspendedUserProtection
  extend ActiveSupport::Concern

  included do
    before_action :verify_active_user
  end

  def verify_active_user
    if current_user.suspended?
      render json: { error: 'Your account is suspended.' }, status: :forbidden
    end
  end
end

Alternatively, for a more flexible approach, we could use Rails’ RequestForgeryProtection as inspiration, where they wrap a before_action with a class method called protect_from_forgery that forwards the options to before_action, like this:

class OrdersController < ApplicationController
  include SuspendedUserProtection

  # We can now specify all normal before_action options here to target this check where
  # needed.
  block_suspended_user only: :index

  def index
    @orders = current_user.orders
    render :index
  end
end

module SuspendedUserProtection
  extend ActiveSupport::Concern

  class_methods do
    def block_suspended_user(options)
      before_action :verify_active_user, options
    end
  end

  def verify_active_user
    if current_user.suspended?
      render json: { error: 'Your account is suspended.' }, status: :forbidden
    end
  end
end

The Fine Print

Can this be abused? Yes. Should you do it blindly? No! But used judiciously, this can absolutely clean up bulky, convoluted controller methods. Happy refactoring!