Search
⌘K
    to navigateEnterto select Escto close

    Use Jbuilder to render JSON

    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.

    Features

    • 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 EditTask and ShowTask 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 lines of code:

    1class TasksController < ApplicationController
    2  before_action :load_task, only: %i[show update destroy]
    3
    4  def index
    5    tasks = Task.all
    6    render status: :ok, json: { tasks: tasks }
    7  end
    8
    9  def create
    10    task = Task.new(task_params)
    11    if task.save
    12      render status: :ok, json: { notice: 'Task was successfully created' }
    13    else
    14      errors = task.errors.full_messages.to_sentence
    15      render status: :unprocessable_entity, json: { error: errors  }
    16    end
    17  end
    18
    19  def show
    20    render
    21  end
    22
    23  def update
    24    if @task.update(task_params)
    25      render status: :ok, json: { notice: 'Successfully updated task.' }
    26    else
    27      render status: :unprocessable_entity,
    28        json: { error: @task.errors.full_messages.to_sentence }
    29    end
    30  end
    31
    32  def destroy
    33    if @task.destroy
    34      render status: :ok, json: { notice: 'Successfully deleted task.' }
    35    else
    36      render status: :unprocessable_entity,
    37        json: { error: @task.errors.full_messages.to_sentence }
    38    end
    39  end
    40
    41  private
    42
    43    def task_params
    44      params.require(:task).permit(:title, :user_id)
    45    end
    46
    47    def load_task
    48      @task = Task.find_by(slug: params[:slug])
    49      unless @task
    50        render status: :not_found, json: { error: t('task.not_found') }
    51      end
    52    end
    53end

    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
    2  json.extract! @task,
    3    :id,
    4    :slug,
    5    :title
    6
    7  json.assigned_user do
    8    json.id @task.user.id
    9    json.name @task.user.name
    10  end
    11end

    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.

    Also notice the use of json.extract!. extract! is a helper method provided by Jbuilder which extracts the mentioned attributes or hash elements from the passed object and turns them into keys of the JSON.

    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  },
    9  task_creator: "Oliver Smith"
    10}

    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 using the extract! helper method.

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

    Nesting assigned_user and task_creator 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.

    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 changes 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
    4
    5  root "home#index"
    6  get '*path', to: 'home#index', via: :all
    7end

    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
    6
    7  root "home#index"
    8  get "*path", to: "home#index", via: :all
    9end

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

    Updating EditTask and ShowTask components

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

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

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

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

    Now, let's commit the changes:

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