Back
Chapters

Keep application controller clean

Search icon
Search Book
⌘K

As a project adds more features, the application_controller.rb file tends to gather more and more code and soon it becomes very big and very messy.

Take a look at this application_controller.rb before the refactoring was done.

As we can see many disjoint entities exist within that file. In the first look itself we can see that it doesn't adhere to the DRY principle.

One method has nothing to do with another method. All the after_action declarations are physically too far from the implementation of the method. For example method set_honeybadger_context is 67 lines apart from the implementation of that method.

Using concerns to keep sanity

Concerns in Rails are like Ruby modules that extend the ActiveSupport::Concern module. Rails controllers come with concerns directory. All modules that reside inside concerns directory are automatically loaded by Rails. It's created by Rails team so that we can put related stuff together as a concern in that directory. So let's try to use it.

Using concerns is another form of keeping code DRY.

Moving functionality into a concern

Let's extract the helper methods for sending back a response for API requests from ApplicationController class and move it inside the api_responders.rb concern.

Let's try the same for authorization related code:

Run the following command to create the api_responders.rb concern:

1touch app/controllers/concerns/api_responders.rb

Now add the following lines of code to the api_responders.rb file:

1module ApiResponders
2  extend ActiveSupport::Concern
3
4  private
5
6    def respond_with_error(message, status = :unprocessable_entity, context = {})
7      is_exception = message.kind_of?(StandardError)
8      error_message = is_exception ? message.record&.errors_to_sentence : message
9      render status: status, json: { error: error_message }.merge(context)
10    end
11
12    def respond_with_success(message, status = :ok, context = {})
13      render status: status, json: { notice: message }.merge(context)
14    end
15
16    def respond_with_json(json = {}, status = :ok)
17      render status: status, json: json
18    end
19end

Similarly let us extract the exception rescuing and its corresponding handler methods out of the ApplicationController to the api_rescuable.rb concern.

Run the following command to create the api_rescuable.rb concern:

1touch app/controllers/concerns/api_rescuable.rb

Now add the following lines of code to the api_rescuable.rb file:

1module ApiRescuable
2  extend ActiveSupport::Concern
3
4  included do
5    rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found
6    rescue_from ActiveRecord::RecordInvalid, with: :handle_validation_error
7    rescue_from ActiveRecord::RecordNotUnique, with: :handle_record_not_unique
8    rescue_from ActionController::ParameterMissing, with: :handle_api_error
9    rescue_from Pundit::NotAuthorizedError, with: :handle_authorization_error
10  end
11
12  private
13
14    def handle_validation_error(exception)
15      respond_with_error(exception)
16    end
17
18    def handle_record_not_found(exception)
19      respond_with_error(exception.message, :not_found)
20    end
21
22    def handle_record_not_unique(exception)
23      respond_with_error(exception)
24    end
25
26    def handle_api_error(exception)
27      respond_with_error(exception, :internal_server_error)
28    end
29
30    def handle_authorization_error
31      respond_with_error(t("authorization.denied"), :forbidden)
32    end
33end

There are a couple of key points to note here:

  • Helper methods declared inside the ApiResponders module are used inside the ApiRescuable module. We can say that the ApiRescuable module is dependent on ApiResponders module but we do not need to include ApiResponders inside ApiRescuable.

    We can do so because, since ActiveSupport::Concern dependencies are gracefully resolved.

  • We have used an included block inside the ApiRescuable module. The included block is available because we are extending the ActiveSupport::Concern module.

    When a class includes a module, the code present inside the included block will be executed within the scope of the including class.

    In the above code, when a class includes the ApiRescuable class, the rescue_from callbacks will be executed within the scope of that class. Whenever an exception occurs inside that class or it's child class, the rescue_from callback will be called.

    The included block usually contains code like callbacks and macros.

Let us now move the authentication functionality out of the ApplicationController to the authenticable.rb concern.

Run the following command to create the authenticable.rb concern:

1touch app/controllers/concerns/authenticable.rb

Open app/controllers/concerns/authenticable.rb and add following code:

1module Authenticable
2  extend ActiveSupport::Concern
3
4  included do
5    before_action :authenticate_user_using_x_auth_token
6  end
7
8  private
9
10    def authenticate_user_using_x_auth_token
11      user_email = request.headers["X-Auth-Email"].presence
12      auth_token = request.headers["X-Auth-Token"].to_s
13      user = user_email && User.find_by!(email: user_email)
14      is_valid_token = auth_token && ActiveSupport::SecurityUtils.secure_compare(user.authentication_token, auth_token)
15      if is_valid_token
16        @current_user = user
17      else
18        respond_with_error(t("session.could_not_auth"), :unauthorized)
19      end
20    end

Let's also modify our TasksController to invoke verify_authorized and verify_policy_scoped methods after certain specific actions:

1class TasksController < ApplicationController
2  after_action :verify_authorized, except: :index
3  after_action :verify_policy_scoped, only: :index
4  #------previous code -------
5  end

verify_authorized raises an error if pundit authorization has not been performed in specified actions. That is why we invoke it as an after_action hook. It is used to prevent the programmer from forgetting to call authorize from specified action methods.

Like verify_authorized, Pundit also adds verify_policy_scoped to our controller. It tracks and makes sure that policy_scope is used in the specified actions. This is mostly useful for controller actions like index which find collections with a scope and don't authorize individual instances.

Sanitized version

In order to make the application_controller even thinner and neater, we just need to include the necessary concerns, rather than defining the functionality with the controller.

For example, in a fully fledged application, once all the code is moved to concerns, then the application_controller.rb would look something like this (no need to add the following changes):

1class ApplicationController < ActionController::Base
2  include Authenticable
3  include Authorizable
4  include ApiException
5  include SetHoneybadgerContext
6  include RedirectHttpToHttps
7  include EnsureTermsOfServiceIsAccepted
8  include EnsureUserOnboarded
9  include DataLoader
10end

Now let's modify our current application_controller and include the concerns we created in the previous section. Fully replace the contents of application_controller.rb file like so:

1class ApplicationController < ActionController::Base
2  include ApiResponders
3  include ApiRescuable
4  include Authenticable
5  include Pundit
6
7  private
8
9    def current_user
10      @current_user
11    end
12end

The @current_user is assigned when the authenticate_user_using_x_auth_token inside the Authenticable concern is invoked when the server receives an API request.

By convention, we prefer to name concerns with able suffix. It suggests the addition of ability.

An important point to keep in mind is that we need to use concerns only when the logic has to be shared within multiple controllers or related files.

If the logic is only specific to a controller, then we can either write it in the private section or move it inside a helper.

Now let's commit these changes, where we moved pundit related functionalities into Authorizable concern:

1git add -A
2git commit -m "Moved functionality from application controller to concerns"