Jbuilder

Jbuilder helps you to construct templates for endpoint responses.

Let's look at the BooksController we've defined earlier.

In app/controllers/books_controller.rb,

class BooksController < ApplicationController
  def create
    new_book = Book.create(name: params[:name])

    render json: { book: new_book }
  end
end

Here, we are specifically defining the response. We say that the format should be in json and the value would be { book: new_book }. But, MVC is about Model, View, Controller. We would like to separate these 3 things. And, in this case, we are mixing the view into the controller.

So, let's try to separate the View from the Controller. In Rails, we can use another gem called jbuilder to help us construct templates of responses in JSON. We can also REUSE these templates in other controllers, making our program MODULAR.

In programming, there is a concept called Don't Repeat Yourself, DRY for short. Which means if you are repeating the same lines of code in different parts of your program, you have an opportunity to turn that code into its own module. Just like a class or a method. Then different parts of your program will be calling/invoking this module instead.

In fact, jbuilder is installed in Rails by default. You can check the Gemfile to see it.

So, how do we use jbuilder? Let's modify the controller, so it will look for a jbuilder file when sending back a response.

class BooksController < ApplicationController
  def create
    new_book = Book.create(name: params[:name])

    render 'books/create.jbuilder'
  end
end

render 'books/create.jbuilder' is essentially looking for a file in the app/views folder. The entire path is app/views/books/create.jbuilder. You can see the folder structure follows the controller name books, and then the method name create with the jbuilder file type .jbuilder.

There is another widely used concept called TDD, Test Driven Development. It is a development process where you convert your program requirements into tests, then improve your program to pass those tests. Hence it's called Test Driven. In our case, the tests are our specs. Very stringent TDD followers will first write the tests before they write the programs.

So, why don't we run our test again. Thankfully, we no longer need to do it in Postman. We can run rspec $ rspec

$ rspec
F

Failures:

  1) BooksController POST /books create a new book object
     Failure/Error: render 'books/create.jbuilder'

     ActionView::MissingTemplate:
       Missing template books/create.jbuilder with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]}. Searched in:
         * "../app_name/app/views"
     # ./app/controllers/books_controller.rb:5:in `create'
     # ./spec/controllers/books_controller_spec.rb:6:in `block (3 levels) in <top (required)>'

Finished in 0.14547 seconds (files took 2.33 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/controllers/books_controller_spec.rb:5 # BooksController POST /books create a new book object

Oops, we have 1 failure, as expected. Read the error message carefully.

  1. Failure/Error: render 'books/create.jbuilder': It can't render 'books/create.jbuilder', the code we just modified. Why?
  2. ActionView::MissingTemplate: Template is missing. What template?
  3. Missing template books/create.jbuilder and Searched in: * "../app_name/app/views": Right... we haven't created the create.jbuilder file yet...

Okay, let's create an empty file app/views/books/create.jbuilder.

Let's run rspec again. Now, everything passes.

$ rspec
.

Finished in 0.06381 seconds (files took 3.32 seconds to load)
1 example, 0 failures

We are done, right? No... the file is still empty, yet the test says everything is fine. What's going on? Let's take a look at our spec.

In spec/controllers/books_controller_spec.rb,

require 'rails_helper'

RSpec.describe BooksController, type: :controller do
  describe 'POST /books' do
    it 'create a new book object' do
      post :create, params: {name: 'Harry Potter'}

      expect(Book.count).to eq(1)
    end
  end
end

Well, in this spec, we only expect Book.count to become 1. But, we actually haven't tested the actual response produced by our endpoint. To fix this, let's add another test.

require 'rails_helper'

RSpec.describe BooksController, type: :controller do
  describe 'POST /books' do
    it 'create a new book object' do
      ...
    end

    it 'responds with a book object' do
      post :create, params: {name: 'Harry Potter'}

      expected_response = {
        book: {
          name: 'Harry Potter'
        }
      }

      expect(response.body).to eq(expected_response.to_json)
    end
  end
end

So, we expect the endpoint to respond with a hash with a book object with its name. response.body specify that we want to get the response content. expected_response.to_json is to convert our hash into a json object, so that it can be compared to the actual response body. And, we expect our actual response matches with our expected response. Fair, right?

Since we need to render our views, we need to specify it in our spec. Add render_views on top of the RSpec code block.

RSpec.describe BooksController, type: :controller do
  render_views

  describe 'POST /books' do
    ...
  end
end

Great, let's run this test. $ rspec

$ rspec
.F

Failures:

  1) BooksController POST /books responds with a book object
     Failure/Error: expect(response.body).to eq(expected_response.to_json)

       expected: "{\"book\":{\"name\":\"Harry Potter\"}}"
            got: "{}"

       (compared using ==)
     # ./spec/controllers/books_controller_spec.rb:20:in `block (3 levels) in <top (required)>'

Finished in 0.08396 seconds (files took 3.16 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/controllers/books_controller_spec.rb:11 # BooksController POST /books responds with a book object

So now, 1 specs passed, 1 failed. We got an empty hash. Why? Well, create.jbuilder is still empty. So, we need to fill it.

In app/views/books/create.jbuilder,

json.book do
  json.name new_book.name
end

And, just a reminder what the BooksController looks like:

In app/controllers/books_controller.rb,

class BooksController < ApplicationController
  def create
    new_book = Book.create(name: params[:name])

    render 'books/create.jbuilder'
  end
end

We expect the jbuilder can access the variable new_book in the controller. So, let's run $ rspec.

$ rspec
FF

Failures:

  1) BooksController POST /books create a new book object
     Failure/Error: json.name new_book.name

     ActionView::Template::Error:
       undefined local variable or method `new_book' for #<#<Class:0x007f8c9fc1abf0>:0x007f8c9fc0acc8>
      ...

  2) BooksController POST /books responds with a book object
     Failure/Error: json.name new_book.name

     ActionView::Template::Error:
       undefined local variable or method `new_book' for #<#<Class:0x007f8c9fc1abf0>:0x007f8c9fb02470>
      ...

Finished in 0.10073 seconds (files took 2.6 seconds to load)
2 examples, 2 failures

...

Oops. LOL. The fact is, by default, jbuilder cannot access the variable new_book. If we want to make it accessible across these files, we have to add @ in front.

Why? new_book is a local variable, only accessible within the create method of the controller. @new_book is an instance variable. In rails, declaring your variables in your controller as instance variable (@variable_name) makes them available to your view.

Alright. Let's change that. And, let's change the name @new_book to @book since this is the Rails convention.

In app/views/books/create.jbuilder,

json.book do
  json.name @book.name
end

Why does jbuilder look like this? This format looks weird. Well, this is the syntax jbuilder has designed. You have to follow it in order to use it. Read their documentations on how to use it: https://github.com/rails/jbuilder

In app/controllers/books_controller.rb,

class BooksController < ApplicationController
  def create
    @book = Book.create(name: params[:name])

    render 'books/create.jbuilder'
  end
end

Alright. Usual Biz. Run $ rspec

$ rspec
..

2 examples, 0 failures

YATTA! We did it.

results matching ""

    No results matching ""