In Rails, controllers play a fundamental role as a component of the Model-View-Controller pattern.
They act as intermediaries between models and views by handling user requests and determining the appropriate response to those requests.
Let's start by learning about their roles more in depth:
The role of a controller in Rails
Controllers are responsible for handling the flow of data between the UI, the views, and the data layer (models).
When a request is made to our application, the request is usually routed to a controller action that is responsible for processing the request and returning a response.
In a typical request flow, the controller is responsible for:
- Receiving requests: they receive HTTP requests that can come from users or even other computers, interpret the parameters and determine the appropriate action to execute.
- Interacting with models or other objects: because we would rather not have too much logic lying around in our controllers, they interact with other objects, generally models, to perform the desired operations, especially if we are doing CRUD operations.
- Rendering views: after processing the request, controllers typically render views that are used to present the data to users. By default, views are ERB templates that produce HTML, but they can actually produce JSON, XML and other formats.
- Authorization: controllers are often responsible for checking if a request is permitted in any given scenario and responding in consequence. Even if authorization can happen at a lower level, using a Rack middleware, developers tend to prefer handling it at the controller level.
- Redirections: we can handle these in the router itself or in a middleware but, whenever the redirect is part of the logic, it's preferable that we have them in the controller to avoid causing confusion.
-
Managing sessions and cookies: authentication logic and cookie-bound user state is handled by controllers.
What is a controller in Rails?
In Rails, a controller is just a class that inherits from
ActionController::Base
which follows some conventions: It's defined inside the
controllers
folder.Its name is usually in plural and ends in “Controller” like:
UsersController
orSessionsController
.It defines actions, which are simply Ruby methods, that correspond to a route defined in the
routes.rb
file. However, every action in the router corresponds to a controller action, not every method in a controller has to correspond to a route action because we can define or invoke helper methods to accomplish what we want.Ideally, a controller only defines standalone actions that map to the REST methods:
index
to produce a collection of records,show
to produce the detailed version of a record,new
andedit
to render the forms where records are created or edited,create
andupdate
to actually persist or update records anddestroy
to delete them. Sticking to this convention is not mandatory, but it usually helps us better design our application because it forces us to better extract concepts and entities.By default, a standalone controller action matches to a view in the
views
folder that has the same name as the action. For example, theshow
action in theUsersController
would match to theusers/show.html.erb
in theviews
folder.It provides us with access to a special
render
method which can only be called once for every controller action because it returns a view of some kind.It provides us with access to a
respond_to
method that receives a block, where we can define which request formats we wish to handle and how to handle them.
So, even if a controller is a somewhat special Ruby object, at the end of the day, it's still a Ruby class so we can treat it as such.
Controller structure
A typical controller in Rails might look like this:
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy] # Filters
## GET /products => Returns a collection
def index
@products = Product.recent.page(params[:page]).per(params[:per_page])
end
## GET /products/:id => Returns a single product
def show
end
## GET /products/new => Initializes a new product
def new
@product = Product.new
end
## POST /products => Creates a new product
def create
@product = Product.new(product_params)
if @product.save
redirect_to @product, notice: 'Product was successfully created.'
else
render :new, status: :unprocessable_entity
end
end
## GET /products/:id/edit => Edits an existing product
def edit
end
## PATCH/PUT /products/:id => Updates an existing product
def update
if @product.update(product_params)
redirect_to @product, notice: 'Product was successfully updated.'
else
render :edit, status: :unprocessable_entity
end
end
## DELETE /products/:id => Deletes a product
def destroy
@product.destroy
redirect_to products_url, notice: 'Product was successfully destroyed.'
end
private
# Strong parameters
def product_params
params.expect(product: [:name, :description, :price, :current_count, :active])
end
# Set product for actions
def set_product
@product = Product.find(params[:id])
end
end
Filters or callbacks
Filters, or controller callbacks, provide a way to hook into the controller's lifecycle to extract duplicated logic and maximize code reutilization.
Rails provides us with three different filters:
-
before_action
: runs before the action executes. They're usually used to extract common code from multiple actions. -
after_action
: runs code after the action executes, and it has access to the data that the client will receive. -
around_action
: is executed before and after a controller action. They are useful for things like measuring performance.
Filters are ideal for authentication, authorization, setting common variables, logging, and performance tracking. You can limit filters to specific actions using the only
or except
options.
class ProductsController < ApplicationController
before_action :require_login, except: [:index, :show]
before_action :set_product, only: [:show, :edit, :update, :destroy]
after_action :log_activity, only: [:create, :update, :destroy]
private
def require_login
redirect_to login_path, alert: "Please log in first" unless current_user
end
def set_product
@product = Product.find(params[:id])
end
def log_activity
ActivityLogger.log(current_user, "#{action_name} product ##{@product.id}")
end
end
As useful as they are, don't overuse them because they can easily obscure the flow of execution and make the code confusing and harder to debug in general.
Controller best practices
The main thing to consider when it comes to defining a controller in Rails is that their role is to operate as a coordinator layer and not so much to handle everything needed to produce the response.
So, considering this, some standard best practices are to write maintainable code are:
- Keep controllers skinny: have controllers be as lightweight as possible and delegate most of the logic to other, more appropriate, objects.
- Use strong parameters: avoid security issues by using strong parameters to only allow a list of predefined attributes to be passed to models or the objects that handle the logic or the persistence.
- Avoid duplication: keep your code more maintainable by defining private methods when necessary or extracting common logic to concerns. However, don't use a concern unless you've already identified clear duplication, and beware of relying on private methods to handle logic that should be located elsewhere.
-
Inherit as little as possible: to avoid future confusion, don't inherit too much behavior from other controllers, like
ApplicationController
. Having too much logic in there can be problematic in the long term because abstracted behavior is harder to follow. - Handle errors: don't trust the happy path, and think as hard as you can about possible errors that can arise from executing the code in your controllers. Errors are not necessarily handled entirely in the controller itself but think about them when writing logic in models or other objects.
- Write tests when appropriate: test your endpoints when they're handling complex logic or if they are responsible for a critical part of your application. Tests for endpoints that only declare an instance variable are less valuable. ## Summary Rails controllers are the intermediaries between models and views in the MVC pattern.
They receive HTTP requests, determine appropriate actions, and generate responses.
A controller is a Ruby class inheriting from ActionController::Base
that lives in the controllers folder and follows naming conventions.
Controllers define action methods (index, show, new, create, edit, update, destroy) that correspond to routes and typically map to similarly named views which produce HTML by default but can produce JSON, XML or other formats if needed.
They can use filters like before_action
to share logic across actions, strong parameters to avoid mass assignment attacks and also include reusable code by using concerns.
Apart from coding styles and design criteria, controllers should generally be skinny by delegating business logic elsewhere and making as little work as possible.
It's also advisable to handle errors gracefully to provide adequate feedback to users and avoid degrading the user experience by raising an error, like a 500 error, that might confuse users.