More at rubyonrails.org:

Layouts and Rendering in Rails

This guide covers the rendering features of Action Controller and its relationship with Action View.

After reading this guide, you will know:

  • How to use the various rendering methods built into Rails.
  • How to render views in multiple formats and variants.
  • How to register custom MIME types and render responses using them.
  • How to set the layout in Rails controllers.

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:

  • render to create a full response with a body, usually with a 2xx status code.
  • respond_to to enable rendering of multiple formats based on the HTTP request's Accept header.
  • redirect_to to redirect the user to another path using an HTTP redirect status code.
  • head to 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:

  1. app/views/admin/products/index.html.erb
  2. app/views/admin/index.html.erb
  3. app/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 calling render.
  • 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.erb
  • app/views/books/index.html+desktop.erb
  • app/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 ArticlesController actions will use the main layout.
  • All ProductsController actions will use the product layout.
  • ProfilesController#show will use the profile layout.
  • ProfilesController#index and all other actions in that controller will not use a layout.
  • All other actions across the application will default to the main layout.


Back to top