Duetcode

Introduce renderer module

18 July 2020

Welcome to the fifth chapter of the API-only ruby on rails course. In this chapter, we will introduce the Renderer module for the API controllers. If you haven’t read the previous chapters yet, you can check the content list from the course page.

As you remember from the previous chapters, we have a user creation endpoint as following.

app/controllers/api/v1/users_controller.rb
# frozen_string_literal: true

class Api::V1::UsersController < Api::V1::BaseController
  def create
    @user = User.new(user_params)

    if @user.save
      return render json: @user, status: :created
    end

    render json: @user.errors, status: :unprocessable_entity
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end

Even though we only have one action at the moment, we will have the same behavior for other endpoints. If the desired condition is done successfully, we would like to render the JSON representation of the resource along with the HTTP status code. Otherwise, we want to render the JSON representation of the errors for the resource. If the endpoint has only one possible path like getting the resource information, we will again use the first approach.

Instead of writing render json: @user, status: :created and render json: @user.errors, status: :unprocessable_entity every time in the controllers, we can extract those code blocks to the specific methods called render_object and render_errors. We can then place them to a separate module to be able to use from different API controllers and have a single place to structure all API payloads. It will also make it easy to add metadata information for the payloads, which we will cover in the next chapter.

Create renderer module

We will build the renderer module while refactoring the create action of the users controller, but we will do it with small iterations and check the tests to feel confident enough about our changes.

Let’s start with creating a concern file (app/controllers/concerns/renderer.rb) and implement render_object method as a first step.

app/controllers/concerns/renderer.rb
# frozen_string_literal: true

module Renderer
  def render_object(resource, status = :ok)
    render json: resource, status: status
  end
end

With the render_object method, we basically get the resource object and render them as we were doing in the users controller. We also used :ok status as a default parameter, since most of the API responses will return HTTP 200 status code. Now let’s use the render_object method from the users controller. (Do not forget to include renderer module!)

app/controllers/api/v1/users_controller.rb
# frozen_string_literal: true

class Api::V1::UsersController < Api::V1::BaseController
  include Renderer

  def create
    @user = User.new(user_params)
    
    if @user.save
      return render_object(@user, :created)
    end

    render json: @user.errors, status: :unprocessable_entity
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end

Now, we can run user request spec to see if everything still works as before.

~/code/bookmarker (master) $ bundle exec rspec spec/requests/api/v1/users_spec.rb
..

Finished in 0.07713 seconds (files took 0.76363 seconds to load)
2 examples, 0 failures

Since everything works as expected, we can do the same enhancement with the render_errors method.

app/controllers/concerns/renderer.rb
# frozen_string_literal: true

module Renderer
  def render_object(object, status = :ok)
    render json: object, status: status
  end

  def render_errors(errors, status = :unprocessable_entity)
    render json: errors, status: status
  end
end

Here we passed unprocessable_entity as a default value of the status parameter, which is 422 HTTP status code. And again, let’s refactor the create action of the users controller.

app/controllers/api/v1/users_controller.rb
# frozen_string_literal: true

class Api::V1::UsersController < Api::V1::BaseController
  include Renderer

  def create
    @user = User.new(user_params)

    if @user.save
      return render_object(@user, :created)
    end

    render_errors(@user.errors)
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end

And again, let’s run user request spec to see tests are still green.

~/code/bookmarker (master) $ bundle exec rspec spec/requests/api/v1/users_spec.rb
..

Finished in 0.07368 seconds (files took 0.73671 seconds to load)
2 examples, 0 failures

All tests are passed, perfect! As you can see, we refactor our create action confidently since we have proper request tests. As a general rule of thumb, it’s good to check whether you have corresponding specs before refactoring any place in the codebase.

We included the renderer module inside of the users controller, but we need to move it to the base controller because we will use that functionality also from other API controllers, which are extended by the base controller.

app/controllers/api/v1/base_controller.rb
# frozen_string_literal: true

class Api::V1::BaseController < ApplicationController
  include Renderer
end
Write tests for the renderer module

We already checked requests specs for the user controller but we also need to write necessary tests for the renderer module itself. Let’s create spec/concerns/renderer_spec.rb file and start with writing spec for the render_object method.

spec/concerns/renderer_spec.rb
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Renderer, type: :controller do
  controller(ApplicationController) do
    include Renderer

    def show
      user = User.find(params[:id])
      render_object(user)
    end
  end

  describe 'GET show' do
    let(:resource) { create(:user) }

    it 'renders resource with render_object method' do
      get :show, params: { id: resource.id }

      resource_fields = { 'id' => resource.id, 'email' => resource.email }

      expect(response.status).to eq(200)
      expect(load_body(response)['user']).to include(resource_fields)
    end
  end
end

Here, we created an anonymous rails controller extended by ApplicationController and included the Renderer module. Then we built a show action that uses the render_object method with the user resource. And later, we defined the controller spec for the anonymous controller we created, and we sent a request to check if the render_object method returns the expected resource payload along with the correct HTTP status code.

Now we can extend same approach for the render_errors method.

spec/concerns/renderer_spec.rb
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Renderer, type: :controller do
  controller(ApplicationController) do
    include Renderer

    # ...

    def create
      user = User.create(user_params)
      render_errors(user.errors)
    end

    private

    def user_params
      params.require(:user).permit(:email, :password)
    end
  end

  # ...

  describe 'POST create' do
    it 'renders resource errors with render_errors method' do
      post :create, params: { user: { email: nil, password: nil } }

      error_fields = {
        'email' => ['can\'t be blank'],
        'password' => ['can\'t be blank']
      }

      expect(response.status).to eq(422)
      expect(load_body(response)).to eq(error_fields)
    end
  end
end

We tried to create a user record with invalid parameters with the create action, and then we returned validation errors with the render_errors method. Similar to the render_object test, we send a request to create action and checked if it has the correct payload with the status code.

Let’s run renderer specs to see everything is working as intended.

~/code/bookmarker (master) $ bundle exec rspec spec/concerns/renderer_spec.rb
..

Finished in 0.07072 seconds (files took 0.80828 seconds to load)
2 examples, 0 failures

You might have realized that we have a Metrics/BlockLength rubocop offense for renderer_spec.rb file since rspec block has too many lines. But since it’s actually the DSL of the rspec, I want to exclude spec directory from this rule.

.rubocop.yml
inherit_from: .rubocop_todo.yml

# ...

Metrics/BlockLength:
  Exclude:
    - 'spec/**/*'

Before finishing the chapter, let’s run all specs to make sure we didn’t break any existing behavior of the application.

~/code/bookmarker (master) $ bundle exec rspec
..........

Finished in 0.11616 seconds (files took 0.87333 seconds to load)
10 examples, 0 failures

Summary

In this chapter, we introduced the renderer module and refactored the create action of users controller by using render_object and render_errors methods. In the next chapter, we will improve the renderer module to have a better API payload structure, and we will add metadata information to the API responses. You can find the source code of bookmarker application on github. You can also find the previous chapter here.

Thank you for reading! If you want to be notified when I publish a new chapter, you can follow me on twitter.