Use Jbuilder to render JSON

Search icon
Search Book

In previous chapters we learnt how to use APIs to perform various CRUD operations and interact with our backend. After each operation the backend responds with a JSON of either relevant data, success message or errors. But we still seem to be missing a big part of how Rails applications are supposed to be built.

Rails is an MVC application framework. To give you a quick refresher MVC stands for Model-View-Controllers. We have models and controllers in our application but what about views? We haven't used any views in our application except for index action of HomeController, which renders an HTML view.

Views are responsible for displaying the results of a controller action in a user friendly manner. In our application we have delegated that job to React.

In Rails, views can do more than data presentation and user interaction. We can also use view templates to compile a JSON response and send it to the client side for React to use the JSON and display the data.

In the previous chapter, we added a feature to assign users to task and display the task assignee information on show task page.

We updated the show action of TasksController to render a JSON of task details such as the task object, assigned_user. It gets the job done but it is not the most efficient way to respond with a JSON.

The JSON data we responded with didn't contain too many keys. But what about the case where we have to declare relatively larger JSON response structures?

Declaring a JSON response inside controllers is not a good practice for a couple of reasons, like the following:

  • Controllers should only be responsible for producing the appropriate output when requests arrive. Rendering JSONs should be delegated to view templates.

  • Declaring JSONs inside controllers will make the controllers grow in size. We should aim to keep controllers as thin as possible.

In this chapter we will see how we can use Jbuilder to declare JSON response structures outside of controllers in the view templates. It is a gem that ships with Rails and it is a great tool for declaring JSON structures.

Jbuilder is quite useful when it comes to generating and rendering JSON responses for API requests in Rails. It provides a number of benefits over declaring JSONs inline which we will see in the coming sections of this chapter.


  • We will separate the job of fetching a task and responding with a JSON of task details, between the show action of TasksController and it's corresponding view template.

  • Fetched task details should be passed on to the view template using instance variables. Instance variables declared inside a controller action are mutually accessible to the controller action as well as to that action's corresponding view template.

Technical design

  • We will query the requested task inside load_task! callback which is called before the show action.

  • We will create a view template for show action under the app/views/tasks folder.

  • We will be able to access the task information inside the corresponding view template.

  • Inside the newly created Jbuilder view template for show action, we will declare our JSON response structure derived from task and subsequently create an HTTP response with the JSON.

  • We will add a default response format for RESTful routes since we require them to respond in JSON format.

  • We will update the Edit and Show components in frontend to support the updated task JSON received as response from the show task API call.

Updating TasksController to render Jbuilder template

We have already taken care of fetching the task in load_task! callback which is invoked before the show action. Let's update the show action to render it's corresponding view template.

To do so update the show action in the TasksController with the following line of code:

1class TasksController < ApplicationController
2  # previous code
4  def show
5    render
6  end
8  private
10    # previous code

Inside the show action we have called the render method to render its associated view template. We can also skip the call to render.

Rails being a developer friendly and productivity oriented framework takes care of this. Even if we skip the call to render, Rails still renders the associated view template.

You should only skip explicitly invoking the render method, as long as you're sure that a view file exists for the corresponding action. Else Rails will raise a template error.

We don't need to pass the :ok status as well because we will delegate the task of declaring and responding with JSON data to Jbuilder template and by default Jbuilder always sends an :ok status with the response unless specified otherwise.

Adding Jbuilder template for show action of TasksController

Before we move on to creating a Jbuilder template for the show action, it is important to understand how to name a Jbuilder template file. It is very similar to the naming convention for views in Rails.

The view template will share its name with the associated controller action followed by the .json.jbuilder file extension. For example, the show controller action of the tasks_controller.rb will use the show.json.jbuilder view file in the app/views/tasks directory.

Let's create the Jbuilder template using the following command:

1touch ./app/views/tasks/show.json.jbuilder

Add the following lines of code to /app/views/tasks/show.json.jbuilder

1json.task do
3  json.slug @task.slug
4  json.title @task.title
6  json.assigned_user do
9  end

As we discussed while introducing Jbuilder, all instance variables in scope of the corresponding controller action can be accessed in the view template. Hence we can access the @task object.

Note that, to declare a nested JSON, we can wrap the nested JSON keys inside the nesting key with the help of a do...end block. In the above code, all the task details are nested inside the task key. Similarly for assigned_user also, we can use a block to declare a nested JSON object with assigned user details.

If you'd like to learn more about the do..end block in Ruby, then please go through the Ruby block lesson in BigBinary Academy.

All .jbuilder view files end up outputting a JSON response. The show.json.jbuilder view file will return a JSON structure similar to the one depicted below:

1task: {
2  id: "h4nid45udi131h44uh41",
3  slug: "pay-bills",
4  title: "Pay bills"
5  assigned_user: {
6    id: "buw48wrbdbao48292bur",
7    name: "Eve Smith"
8  }

In the above JSON structure, only id, slug and title attributes of the task record are included since we had extracted only these particular attributes.

Also, notice that the assigned_user object is nested inside the task object.

Nesting assigned_user inside the task object has made this JSON structure much more easier to comprehend and manipulate. The assigned_user refers to the user who is assigned to the specific task we are dealing with.

Whereas, a separate JSON for assigned_user would have been a bit confusing to someone who doesn't know about the application. They would find it difficult to figure out what assigned_user is supposed to be.

The above JSON structure example shows how Jbuilder allows us to declare meaningful JSON structures with ease.

By now you should be getting a picture of how Jbuilder works. You can read more about Jbuilder and it's features from the official Jbuilder page.

Using extract! method

In the above snippet while we have achieved what we wanted, but the code is repetitive and goes against the DRY principle. Fortunately Jbuilder provides us the helper method extract! that helps us solve this problem.

Let's take another look at our previous snippet:

1json.task do
3  json.slug @task.slug
4  json.title @task.title
6  json.assigned_user do
9  end

Since we can see that the desired keys for our JSON response are identical to the attribute names of our @task model instance, we can use the extract! method which extracts the mentioned attributes or hash elements from the passed object and turns them into keys of the JSON.

We can write the above snippet like so using the extract method:

1json.task do
2  json.extract! @task,
3    :id,
4    :slug,
5    :title
7  json.assigned_user do
8    json.extract! @task.assigned_user,
9      :id,
10      :name
11  end

This will return the exact response as before:

1task: {
2  id: "h4nid45udi131h44uh41",
3  slug: "pay-bills",
4  title: "Pay bills"
5  assigned_user: {
6    id: "buw48wrbdbao48292bur",
7    name: "Eve Smith"
8  }

Updating default response format

The default response format in Rails is text/html whereas we want our application to respond in a JSON format.

Before adding the Jbuilder for show action, we had been rendering a JSON response from the controller action itself. We were explicitly telling Rails to respond in JSON format when we passed an argument named json to the render method of each action.

However, after adding the Jbuilder template, we have removed the render call and because we haven't mentioned a response format, Rails will fallback to the default response format and look for text/html data to respond with.

This will cause Rails to throw an exception for missing template because we have a template that responds with JSON data and Rails, because of it's default response format will require text/html data to respond with.

We can fix this by adding a default response format in our routes config file.

Make the following change in config/routes.rb:

1Rails.application.routes.draw do
2  resources :tasks, except: %i[new edit], param: :slug, defaults: { format: 'json' }
3  resources :users, only: :index
5  root "home#index"
6  get '*path', to: 'home#index', via: :all

We have specified the default response format for all requests on tasks resources.

But the above-mentioned format is not a scalable way to do that particular job. This is where the defaults block comes in handy.

The defaults block can be used to define the defaults, like say format of the response, etc, for multiple routes.

Since we are using Rails to build API endpoints which must respond in JSON format, let's go ahead and add a default json response format for all the RESTful routes in our application.

Usually we use the api/v1 namespace to denote these API routes. But for simplicities sake we are setting the defaults without namespace.

Update config/routes.rb with the following lines of code:

1Rails.application.routes.draw do
2  defaults format: :json do
3    resources :tasks, except: %i[new edit], param: :slug
4    resources :users, only: :index
5  end
7  root "home#index"
8  get "*path", to: "home#index", via: :all

Note that we haven't added a default response format for root path because index action of the HomeController responds with an HTML view.

Adding lambda route constraints

While the defaults block works, there is still an issue. If we visit the /tasks resource, like localhost:3000/tasks, in the browser we will be seeing a JSON response on the screen even when the request format is not JSON. This could cause problems with our app. For example, if we have a route /tasks/create and if a user visits the route or refreshes the page from that URL, then the browser will show a JSON response instead, where we are expecting the browser to show the task creation form.

This is because the format is a constraint that is optional by default. What this means is that the routes under defaults format: :json will match GET requests as well because the format is optional by default.

For example, a GET /tasks request without the format as JSON, will match with tasks#index block, instead of redirecting to home#index as we would expect.

One way to solve this issue is to use a lambda to explicitly match to JSON requests. To implement that we need to update config/routes.rb like so:

1Rails.application.routes.draw do
2  constraints(lambda { |req| req.format == :json }) do
3    resources :tasks, except: %i[new edit], param: :slug
4    resources :users, only: :index
5  end
7  root "home#index"
8  get "*path", to: "home#index", via: :all

Using lambda {|req| req.format==:json} as the constraint will make sure that only JSON requests will match with the resources listed in the block.

You can learn more about constraints from Ruby on Rails guide on Rails Routing from the Outside In.

Since we have made sure that only JSON requests get matched to our resources, we need to make sure to explicitly set Accept and Content-Type to application/json in our request headers.

You might remember doing so in setAuthHeaders method of our axios.js file in the Setting up React environment chapter of this book.

The content of our setAuthHeaders method is as follows:

1// Remaining code
2export const setAuthHeaders = (setLoading = () => null) => {
3  axios.defaults.headers = {
4    Accept: "application/json",
5    "Content-Type": "application/json",
6    "X-CSRF-TOKEN": document
7      .querySelector('[name="csrf-token"]')
8      .getAttribute("content"),
9  };
10  const token = localStorage.getItem("authToken");
11  const email = localStorage.getItem("authEmail");
12  if (token && email) {
13    axios.defaults.headers["X-Auth-Email"] = email;
14    axios.defaults.headers["X-Auth-Token"] = token;
15  }
16  setLoading(false);
18// Remaining code

This is done to make sure only JSON requests get matched with resources and all HTML requests get redirected to home#index.

Updating Edit and Show components

To support the updated JSON response from show task API call, update the Edit.jsx with the following lines of code:

1import React, { useState, useEffect } from "react";
3import tasksApi from "apis/tasks";
4import usersApi from "apis/users";
5import Container from "components/Container";
6import PageLoader from "components/PageLoader";
7import { useParams } from "react-router-dom";
9import Form from "./Form";
11const Edit = ({ history }) => {
12  const [title, setTitle] = useState("");
13  const [userId, setUserId] = useState("");
14  const [assignedUser, setAssignedUser] = useState("");
15  const [users, setUsers] = useState([]);
16  const [loading, setLoading] = useState(false);
17  const [pageLoading, setPageLoading] = useState(true);
18  const { slug } = useParams();
20  const handleSubmit = async event => {
21    event.preventDefault();
22    try {
23      await tasksApi.update({
24        slug,
25        payload: { title, assigned_user_id: userId },
26      });
27      setLoading(false);
28      history.push("/dashboard");
29    } catch (error) {
30      setLoading(false);
31      logger.error(error);
32    }
33  };
35  const fetchUserDetails = async () => {
36    try {
37      const {
38        data: { users },
39      } = await usersApi.list();
40      setUsers(users);
41    } catch (error) {
42      logger.error(error);
43    }
44  };
46  const fetchTaskDetails = async () => {
47    try {
48      const {
49        data: {
50          task: { title, assigned_user },
51        },
52      } = await;
53      setTitle(title);
54      setAssignedUser(assigned_user);
55      setUserId(;
56    } catch (error) {
57      logger.error(error);
58    }
59  };
61  const loadData = async () => {
62    await Promise.all([fetchTaskDetails(), fetchUserDetails()]);
63    setPageLoading(false);
64  };
66  useEffect(() => {
67    loadData();
68  }, []);
70  if (pageLoading) {
71    return (
72      <div className="w-screen h-screen">
73        <PageLoader />
74      </div>
75    );
76  }
78  return (
79    <Container>
80      <Form
81        type="update"
82        title={title}
83        users={users}
84        assignedUser={assignedUser}
85        setTitle={setTitle}
86        setUserId={setUserId}
87        loading={loading}
88        handleSubmit={handleSubmit}
89      />
90    </Container>
91  );
94export default Edit;

Now, fully replace the Show.jsx with the following lines of code:

1import React, { useState, useEffect } from "react";
2import { useParams, useHistory } from "react-router-dom";
4import Container from "components/Container";
5import PageLoader from "components/PageLoader";
6import tasksApi from "apis/tasks";
8const Show = () => {
9  const [task, setTask] = useState([]);
10  const [pageLoading, setPageLoading] = useState(true);
11  const { slug } = useParams();
13  let history = useHistory();
15  const updateTask = () => {
16    history.push(`/tasks/${task.slug}/edit`);
17  };
19  const fetchTaskDetails = async () => {
20    try {
21      const {
22        data: { task },
23      } = await;
24      setTask(task);
25    } catch (error) {
26      logger.error(error);
27    } finally {
28      setPageLoading(false);
29    }
30  };
32  useEffect(() => {
33    fetchTaskDetails();
34  }, []);
36  if (pageLoading) {
37    return <PageLoader />;
38  }
40  return (
41    <Container>
42      <h1 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-gray-800 border-b border-gray-500">
43        <span className="text-gray-600">Task Title : </span> {task?.title}
44      </h1>
45      <div className="bg-bb-env px-2 mt-2 mb-4 rounded">
46        <i
47          className="text-2xl text-center transition cursor-pointer duration-300ease-in-out ri-edit-line hover:text-bb-yellow"
48          onClick={updateTask}
49        ></i>
50      </div>
51      <h2 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-gray-800 border-b border-gray-500">
52        <span className="text-gray-600">Assigned To : </span>
53        {task?}
54      </h2>
55    </Container>
56  );
59export default Show;

Now, let's commit the changes:

1git add -A
2git commit -m "Added Jbuilder template for show action in TasksController"