1. Introduction
In this guide, we'll focus on the relationship between the Controller and the View, and the process of rendering a response to send back to the client. You'll need a basic understanding of HTTP requests and responses, Action View, Rails controller conventions, and Rails routing.
The controller is responsible for orchestrating how an HTTP request is handled in Rails. It reads the parameters from an incoming request, then hands off to the model layer for any complex business logic. Finally, it hands off to the view to send a response back to the user.
There are four ways to create an HTTP response in a Rails controller:
renderto create a full response with a body, usually with a2xxstatus code.respond_toto enable rendering of multiple formats based on the HTTP request'sAcceptheader.redirect_toto redirect the user to another path using an HTTP redirect status code.headto create a response consisting solely of HTTP headers without a body.
In this guide, we will focus on the render and respond_to methods. Consult
the Action Controller guide for details on
redirect_to and head.
2. Rendering Responses
The render method is the primary workhorse to create HTTP
responses in Rails. It's used to create a fully-formed response with a body
which usually contains an HTML document.
2.1. The Basics
Consider the below controller class and the route pointing to it. We also have a
view for the index action.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
render "index"
end
end
# config/routes.rb
Rails.application.routes.draw do
resources :books
end
<%# app/views/books/index.html.erb -%>
<h1>Books are coming soon!</h1>
Rails will automatically look for the specified view in the corresponding view
folder for the controller. Navigating to /books will render the heading "Books
are coming soon!".
Next, we enhance the action by loading all Book records from our database to
render in the view.
class BooksController < ApplicationController
def index
@books = Book.all
render "index"
end
end
The view to display all the books might look like:
<h1>Books</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Synopsis</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.name %></td>
<td><%= book.synopsis %></td>
<td><%= link_to "Show", book %></td>
<td><%= link_to "Edit", edit_book_path(book) %></td>
<td><%= link_to "Destroy", book, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to "New book", new_book_path %>
We'll now see this table at /books.
2.1.1. Conventional Rendering
Rails will conventionally render the view with the same name as the action —
render doesn't need to be called manually.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
# Automatically renders `app/views/books/index.html.erb`
def index
@books = Book.all
end
end
You only need to explicitly call render in the action to customize responses
as described in the next section.
2.2. Rendering Templates
render accepts options to to render a different template within a controller
action, or set the HTTP response code.
def update
@book = Book.find(params[:id])
if @book.update(book_params)
redirect_to @book
else
render "edit", status: :unprocessable_content
end
end
You can also use a symbol instead of a string to specify the name of the template:
render :edit, status: :unprocessable_content
You can use the action: option to be more explicit:
render action: :edit, status: :unprocessable_content
We render with the HTTP status
303 Unprocessable Content
in the above example because it's the idiomatically correct HTTP response code
for a form submission error. It's also required by the
Turbo JavaScript library which Rails includes by
default.
To render a template which belongs to a different controller, you can use its
relative path from the app/views/ directory. For example, render
app/views/products/show.html.erb from the BooksController using:
render "products/show"
Optionally, you can include the template: keyword argument:
render template: "products/show"
render :edit, render action: :edit, and render template: :edit are
all functionally equivalent. Rails normalizes all 3 method signatures under the
hood meaning they follow the same code path.
Rendering a template for a different controller action does not call the method corresponding to that controller action. Consider the below example:
def edit
@book = Book.find(params[:id])
end
def update
render "edit", status: :unprocessable_content
end
render "edit" will not call the edit method. It will render the
edit.html.erb template from the context of the update action, meaning
@book will not be set when the update action is rendered.
2.3. Template Lookup Hierarcy
When there isn't an explicit call to render, Rails follows the controller's
inheritance chain to find the appropriate template. Consider the below
controllers:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
end
# app/controllers/admin_controller.rb
class AdminController < ApplicationController
end
# app/controllers/admin/products_controller.rb
class Admin::ProductsController < AdminController
def index
end
end
The lookup order for an admin/products#index action will be:
app/views/admin/products/index.html.erbapp/views/admin/index.html.erbapp/views/application/index.html.erb
If the template is missing from all these locations, an
ActionView::MissingTemplate error will be raised.
2.4. Inline rendering
The render method can be used to define the response body within the
controller itself. This technique can be used to render reponses in a variety of
formats such as plain text, JSON, XML etc.
A key difference when rendering inline is that the response is rendered without
a layout by default. For HTML and plain
text responses, you can use the layout: option to explicity define a layout
template; but, all other formats have
custom renderers
which don't incorporate a layout.
Let's look at some examples to understand what this means.
2.4.1. HTML
Use the html: option to render an HTML string inline.
render html: helpers.tag.strong("Not Found")
The response will be rendered without a layout by default. Use the layout:
option to render within a layout template:
# Renders within the default layout for the controller,
# usually: `app/views/layouts/application.html.erb`.
render html: helpers.tag.strong("Not Found"), layout: true
# Explitly define rendering within the `app/views/layouts/admin.html.erb`
# layout template.
render html: helpers.tag.strong("Not Found"), layout: "admin"
There's rarely a good reason to do this in practice. Use a template file to render HTML responses.
When using the html: option, HTML entities will be escaped if the
string is not composed with html_safe-aware APIs.
2.4.2. Plain Text
You can send plain text, without markup, back to the browser:
render plain: "OK"
To wrap this response in a layout, you'll need a .text.erb layout file and
then use the layout: option:
# Inserts the text "OK" into the controller's default layout, usually:
# `app/views/layouts/application.text.erb`
render plain: "OK", layout: true
# Inserts the text "OK" into `app/views/layouts/plain_text_wrapper.text.erb`
render plain: "OK", layout: "plain_text_wrapper"
2.4.3. JSON
Use the json: option to format JSON reponses. The supplied object will be
converted to JSON using to_json:
render json: @product
2.4.4. XML
XML can be rendered with the xml: option. The supplied object will be
converted to XML using to_xml:
render xml: @product
2.4.5. Raw Body
You can create a response using a raw string using the body: option:
render body: "raw"
There's unlikely to be a practical scenario where this option fits the bill. Use one of the alternate rendering options such as JSON or XML to create stucturally sound reponses and all the security benefits that come with it.
2.4.6. Files
Rails can render a file from an absolute path. This is useful for rendering static files like error pages. ERB and other templating engines are not supported in files passed to this option.
render file: "#{Rails.root}/public/404.html"
If a layout file matching the file's format exists, it will be rendered within
that layout by default. For example, the above example will render the
404.html page within the app/views/layouts/application.html.erb layout. This
can be disabled using layout: false:
render file: "#{Rails.root}/public/404.html", layout: false
The layout that a static file is inserted into can written using a
templating language like ERB. However, the file supplied to the file: option
must be static. It will not be processed through a templating engine.
Using the :file option in combination with users input can lead to
security problems since an attacker could use this action to access security
sensitive files on your file system.
send_file is
often a faster and better option if a layout isn't required.
2.4.7. Rendering Objects
Rails can render objects responding to render_in. The render_in method
signature must accept the view_context (usually an instance of
ActionView::Base), and arbitrary keyword arguments which can be defined to set
local variables.
The object must also define a format method that returns a valid
Mime key.
See the Custom MIME types section below for further details on Mime keys.
class Greeting
include ActionView::Helpers::TagHelper
def render_in(view_context, **options)
name = options[:locals][:name] || "world"
view_context.render html: tag.h1("Hello, #{name}")
end
def format
:html
end
end
Render the above object in a controller action using:
render Greeting.new
# => <h1>Hello, world</h1>
Pass a local variable to the object as:
render Greeting.new, name: "Rails"
# => <h1>Hello, Rails</h1>
This calls render_in on the provided object with the current view context.
You can specify the renderable: option to be more explicit:
render renderable: Greeting.new, name: "Rails"
# => <h1>Hello, Rails</h1>
Render an ERB template from within your renderable object as:
class Greeting
def render_in(view_context, **)
view_context.render(inline: <<~ERB.strip, **)
<h1>Hello, <%= local_assigns[:name] || "world" %></h1>
ERB
end
def format
:html
end
end
render Greeting.new
# => <h1>Hello, world</h1>
render Greeting.new, name: "Rails"
# => <h1>Hello, Rails</h1>
2.4.8. Inline Templating
ERB or a similar templating string can be rendered inline using the inline:
option:
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
Other templating engines can be used with the type: option:
render inline: "xml.p 'This is a bad idea...'", type: :builder
render inline: "json.content 'This is a bad idea...'", type: :jbuilder
There is seldom any good reason to use this technique. Mixing templating logic into your controllers defeats the MVC orientation of Rails and will make it harder for other developers to follow the logic of your project. Use a separate view instead.
2.5. Customizing Responses
HTTP responses can be customized by passing additional options to
render.
2.5.1. content_type:
The content_type: option sets the
content-type
HTTP header for the response. It needs to be set to a valid
MIME type
so the browser understands how the response is formatted.
Rails automatically sets this in most cases. For example, rendering an HTML ERB
template will set the content type to text/html, and rendering a JSON object
will set it to application/json.
It can be explicity set if needed:
render template: "feed", content_type: "application/rss"
2.5.2. location:
Use the location option to set the HTTP
Location
header:
render plain: "Redirecting...", location: root_path, status: :see_other
While this is valid code, using
redirect_to
is the canonical way to send redirect responses in Rails.
2.5.3. status:
Rails automatically sets the HTTP status code — in most cases, this is 200 OK.
Change it by supplying a status::
render status: 500
render status: :forbidden
Rails understands both numeric status codes and the corresponding symbols shown below.
| Response Class | HTTP Status Code | Symbol |
|---|---|---|
| Informational | 100 | :continue |
| 101 | :switching_protocols | |
| 102 | :processing | |
| 103 | :early_hints | |
| Success | 200 | :ok |
| 201 | :created | |
| 202 | :accepted | |
| 203 | :non_authoritative_information | |
| 204 | :no_content | |
| 205 | :reset_content | |
| 206 | :partial_content | |
| 207 | :multi_status | |
| 208 | :already_reported | |
| 226 | :im_used | |
| Redirection | 300 | :multiple_choices |
| 301 | :moved_permanently | |
| 302 | :found | |
| 303 | :see_other | |
| 304 | :not_modified | |
| 305 | :use_proxy | |
| 307 | :temporary_redirect | |
| 308 | :permanent_redirect | |
| Client Error | 400 | :bad_request |
| 401 | :unauthorized | |
| 402 | :payment_required | |
| 403 | :forbidden | |
| 404 | :not_found | |
| 405 | :method_not_allowed | |
| 406 | :not_acceptable | |
| 407 | :proxy_authentication_required | |
| 408 | :request_timeout | |
| 409 | :conflict | |
| 410 | :gone | |
| 411 | :length_required | |
| 412 | :precondition_failed | |
| 413 | :content_too_large | |
| 414 | :uri_too_long | |
| 415 | :unsupported_media_type | |
| 416 | :range_not_satisfiable | |
| 417 | :expectation_failed | |
| 421 | :misdirected_request | |
| 422 | :unprocessable_content | |
| 423 | :locked | |
| 424 | :failed_dependency | |
| 426 | :upgrade_required | |
| 428 | :precondition_required | |
| 429 | :too_many_requests | |
| 431 | :request_header_fields_too_large | |
| 451 | :unavailable_for_legal_reasons | |
| Server Error | 500 | :internal_server_error |
| 501 | :not_implemented | |
| 502 | :bad_gateway | |
| 503 | :service_unavailable | |
| 504 | :gateway_timeout | |
| 505 | :http_version_not_supported | |
| 506 | :variant_also_negotiates | |
| 507 | :insufficient_storage | |
| 508 | :loop_detected | |
| 510 | :not_extended | |
| 511 | :network_authentication_required |
The mapping between the codes and symbols is defined within Rack. If you try to render content along with a non-content status code (100-199, 204, 205, or 304), it will be dropped from the response.
2.6. Inspecting Responses
A response body can be inspected using render_to_string. This method takes the
same options as render, but instead of setting the response, it returns a
string containing the response body.
json_response = render_to_string formats: :json
This method doesn't affect the actual rendering of the response. The below
action will still implicitly render the index view:
def index
@books = Book.all
json_response = render_to_string formats: :json
end
render_to_string doesn't return any headers, just the response body. Hence,
options such as status: are ignored.
2.7. Avoiding Double Render Errors
The render method does not return from the current scope. Lines after a
render call will still be executed. Calling render multiple times in the
same controller action is not allowed and will raise a
AbstractController::DoubleRenderError.
For example, this action could trigger a double render error:
def index
@books = Book.all
if Current.user.admin?
render "admin/books/index"
end
render "index"
end
If the user is an admin, the render within the if statement will be called,
but so will render "index".
You can fix this by adding an explicit return:
def index
@books = Book.all
if Current.user.admin?
return render "admin/books/index"
end
render "index"
end
Or by using an else branch:
def index
@books = Book.all
if Current.user.admin?
render "admin/books/index"
else
render "index"
end
end
Implicit rendering is unaffected by this. The controller action will only render
the conventional template if render wasn't called when the action executed.
The below example will not raise a double render error and is functionally
equivalent to the previous example.
def index
@books = Book.all
if Current.user.admin?
render "admin/books/index"
end
end
3. Multi-Format Responses
Rails templates can be written in a variety of formats, not just HTML. For example, consider the below JSON and XML templates:
# app/views/books/index.json.jbuilder
json.array! @books do |book|
json.name(book.name)
json.synopsis(book.synopsis)
end
<%# app/views/books/index.xml.erb -%>
<books>
<% @books.each do |book| %>
<book>
<name><%= book.name %></name>
<synopsis><%= book.synopsis %></synopsis>
</book>
<% end %>
</books>
In these examples, we're building the JSON template using
jbuilder for security and convenience,
but still using ERB for the XML template. Rails also includes
builder, which provides a DSL to create
XML templates if you wish to use it.
These templates can co-exist alongside an HTML ERB template:
<%# app/views/books/index.html.erb -%>
<h1>Books</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Synopsis</th>
</tr>
</thead>
<tbody>
<% @books.each do |book| %>
<tr>
<td><%= book.name %></td>
<td><%= book.synopsis %></td>
</tr>
<% end %>
</tbody>
</table>
This means there are 3 template formats available for the index action on the
BooksController. Without an explicit render, Rails will automatically choose
the correct format based on the request.
The request can control the response format by specifying an Accept HTTP
header with the appropriate
MIME type:
GET /books.json HTTP/1.1
Accept: application/json
or it can set an extension on the path:
/books.json
/books.xml
The default format is HTML. For more fine-grained control over formats, Rails controllers offer two methods:
- Supplying
formats:when callingrender. - Using
respond_to.
3.1. The formats: option
The formats: option on render overrides any definitions in the request and
forces the controller to render the specified format:
render formats: :json
An array can be passed to define fallbacks if the template for a given format doesn't exist. The below example will attempt to render the JSON template, but fallback to XML if it doesn't exist.
render formats: [:json, :xml]
An ActionView::MissingTemplate error is raised when a template with the
required format doesn't exist.
3.2. Advanced Format Handling Using respond_to
respond_to explicitly defines the formats available to the controller.
class BooksController < ApplicationController
def index
@books = Book.all
respond_to :html, :xml, :json
end
end
The above is functionally equivalent to an implicit render when templates for all 3 formats exist:
class BooksController < ApplicationController
def index
@books = Book.all
end
end
It can also be expressed using a block:
class BooksController < ApplicationController
def index
@books = Book.all
respond_to do |format|
format.html
format.json
format.xml
end
end
end
The above 3 code snippets are functionally equivalent. However, within this block structure, you can define custom handling for each format. For example, you might want to render a different template for HTML requests:
class BooksController < ApplicationController
def index
@books = Book.all
respond_to do |format|
format.html { render "carousel" }
format.json
format.xml
end
end
end
HTML requests will render carousel.html.erb, whereas JSON and XML requests
will render index.json.jbuilder and index.xml.erb respectively.
You can even redirect requests based on the format:
class BooksController < ApplicationController
def index
@books = Book.all
respond_to do |format|
format.html { redirect_to books_grid_path }
format.json
format.xml
end
end
end
Now requests for HTML pages will be redirected, but JSON and XML requests will be rendered.
You can create a handler for multiple formats using format.any:
class BooksController < ApplicationController
def index
@books = Book.all
respond_to do |format|
format.html { redirect_to books_grid_path }
format.any(:xml, :json)
end
end
end
Omit the format definitions passed to format.any to create a catch-all
handler. The below example will redirect HTML requests and render the
appropriate index template for all other request types, where available.
class BooksController < ApplicationController
def index
@books = Book.all
respond_to do |format|
format.html { redirect_to books_grid_path }
format.any
end
end
end
If the requested format isn't available when using respond_to, Rails will
respond with the HTTP status 406 Not Acceptable.
3.3. Template Variants
Rails allows you to create multiple variants of the same template. Controllers responding to requests from a mobile platform might need to render different content than requests from a desktop browser. One strategy to accomplish this is to set a request variant.
Variant names are arbitrary, and can communicate anything from the request's
platform (:android, :ios, :linux, :macos, :windows) to its device
(:mobile, :desktop), to the type of user (:admin, :guest, :user).
The variant name is included in the file's extension.
app/views/books/index.html+mobile.erbapp/views/books/index.html+desktop.erbapp/views/books/index.html.erb
Render a variant using the variants: option.
class BooksController < ApplicationController
def index
@books = Book.all
# `variants:` accepts a single symbol or an array of symbols
render variants: [:mobile, :desktop]
end
end
Rails will render the first available template variant from the array supplied,
falling back to the default .html.erb if no matching variant is found.
Rails automatically renders the appropriate variant when
request.variant
is set. It's a good idea to add the logic to set the variant to your
ApplicationController so all your controllers get this functionality. Rails
will always render the default template so variants can be added only where
necessary.
# app/controllers/concerns/set_request_variant.rb
module SetRequestVariant
extend ActiveSupport::Concern
included do
before_action :set_request_variant
end
private
def set_request_variant
# App-specific logic to determine which variant is requested
request.variant = # ...
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include SetRequestVariant
end
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
@books = Book.all
end
end
An explicit render variants: call isn't required when using request.variant.
You can add custom handling for variants within a respond_to block:
respond_to do |format|
format.html do |html|
html.desktop { render "dashboard" }
html.mobile
end
format.json
format.xml
end
respond_to do |format|
format.html.desktop { render "dashboard" }
format.html
format.json
format.xml
end
Both the above examples are functionally equivalent. Choose the syntax that is appropriate for your use case.
3.4. Custom MIME types
Rails registers several common
MIME types
by default.
All these types can be used within respond_to. Your application can
additionally define custom MIME types.
# config/initializers/mime_types.rb
Mime::Type.register "text/vnd.my-mime-type", :my_mime_type
The above definition follows the convention for naming custom MIME types. This can now be used to render responses:
respond_to do |format|
format.my_mime_type { render plain: "This is a custom MIME type" }
format.any
end
Even though we're using render plain:, the content-type HTTP header in the
response will be set to text/vnd.my-mime-type since it's a format-specific
handler.
This is exactly how
Rails' integration with
Turbo creates Turbo Stream responses. It registers
the text/vnd.turbo-stream.html MIME type which allows us to create
.turbo_stream.html templates and use format.turbo_stream in respond_to
blocks.
4. Setting Layouts In Controllers
Layouts provide a common structure into which responses are rendered. If a layout isn't explicitly defined, Rails will automatically select it.
4.1. Automatic Layout Selection
All layout files must be placed within app/views/layouts. Rails will first
look for a layout file with the same base name as the controller — for example,
photos.html.erb for a PhotosController. If such a file doesn't exist, it
will fall back to the default application.html.erb.
4.2. Specifying Layouts for Controllers
Explicity define a layout for a controller with the layout declaration.
class ProductsController < ApplicationController
layout "inventory"
# ...
end
With this declaration, all views rendered by the ProductsController will be
inserted into app/views/layouts/inventory.html.erb.
Use a layout declaration in your ApplicationController to change the default
layout for your entire application:
class ApplicationController < ActionController::Base
layout "main"
# ...
end
4.3. Setting Layouts Dynamically
Call the layout method with a symbol denoting a method name to select a layout
dynamically at runtime.
class ProductsController < ApplicationController
layout :products_layout
def show
@product = Product.find(params[:id])
end
private
def products_layout
Current.user.admin? ? "admin" : "products"
end
end
Now, if the current user is a administrator, they'll get the admin-specific layout when viewing a product.
You can also invoke layout with a
Proc or
lambda.
The controller instance will be passed into it.
class ProductsController < ApplicationController
layout ->(controller) { controller.request.xhr? ? "popup" : "application" }
end
4.4. Conditional Layouts
Layouts defined at the controller level support the :only and :except
options. You can use these options to specify layouts for a subset of the
controller's actions.
class ProductsController < ApplicationController
# The `product` layout is used for all actions except `index` and `rss`.
layout "product", except: [:index, :rss]
end
class ProductsController < ApplicationController
# The `product` layout is used only for the `show` and `edit` actions.
layout "product", only: [:show, :edit]
end
4.5. Layout Hierarchy
Layout declarations cascade downward in the hierarchy, and more specific layout declarations always override more general ones. For example:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
layout "main"
end
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
end
# app/controllers/products_controller.rb
class ProductsController < ArticlesController
layout "product"
end
# app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
layout false
def show
@profile = Profile.find(params[:id])
render layout: "profile"
end
def index
@profiles = Profile.all
end
# ...
end
In this application:
- All
ArticlesControlleractions will use themainlayout. - All
ProductsControlleractions will use theproductlayout. ProfilesController#showwill use theprofilelayout.ProfilesController#indexand all other actions in that controller will not use a layout.- All other actions across the application will default to the
mainlayout.