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.
Failure/Error: render 'books/create.jbuilder'
: It can't render'books/create.jbuilder'
, the code we just modified. Why?ActionView::MissingTemplate
: Template is missing. What template?Missing template books/create.jbuilder
andSearched in: * "../app_name/app/views"
: Right... we haven't created thecreate.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.