In this chapter, we are going to start exposing stuff to the outside world, namely the books
resource. To do so, we need to create the books
controller and its first action, index
.
Let’s get started!
We are not going to use any generator for the books
controller. Instead, we are going to build it, piece by piece, both in this chapter and in the following ones. While creating this controller, we will work on creating a set of services for serialization, pagination, sorting as well as a few more things.
For now, let’s start with the basics. Create the file that will hold the books
controller in app/controllers/
.
touch app/controllers/books_controller.rb
And here is the content for this new controller. It only contains the index
action for now; show
, create
, update
and destroy
will follow soon.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
end
end
Before adding anything to this controller, create a new test file named books_spec.rb
in spec/requests/
. You will also need to create the requests
folder.
mkdir spec/requests && touch spec/requests/books_spec.rb
The minimum content for this new test file is available below. As you can see, we already have a block named describe 'GET /api/books'
where we will add the different tests that will check that this resource, when used with the GET
method, is working properly. In the background, Rails will route this resource to the books
controller and the index
action.
# spec/requests/books_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
describe 'GET /api/books' do
end
end
Speaking about routes, we need to create the link between our resource and the controller. Open the config/routes.rb
file and add the following inside.
# config/routes.rb
Rails.application.routes.draw do
scope :api do
resources :books
end
end
We can use rails routes
to check the routes that were registered by Rails with the code we added.
rails routes
Output
Prefix Verb URI Pattern Controller#Action
books GET /api/books(.:format) books#index
POST /api/books(.:format) books#create
book GET /api/books/:id(.:format) books#show
PATCH /api/books/:id(.:format) books#update
PUT /api/books/:id(.:format) books#update
DELETE /api/books/:id(.:format) books#destroy
It seems we’ve got what we need!
Let’s write some tests for the books
resource available at the URI /api/books
. The first test we need is ensuring that the resource returns 200 OK
.
# spec/requests/books_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
describe 'GET /api/books' do
it 'receives HTTP status 200' do
get '/api/books'
expect(response.status).to eq 200
end
end
end
Let’s see what happens when we run this specific test.
rspec spec/requests/books_spec.rb
Failure (RED)
...
Failure/Error: expect(response.status).to eq 200
expected: 200
got: 204
...
We are getting 204 No Content
back because, currently, the index
action does not return anything. That’s obviously not what we want, so let’s add the bare minimum to make this test pass.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
render json: {}
end
end
Now, re-run the test.
rspec spec/requests/books_spec.rb
Success (GREEN)
Finished in 0.17294 seconds (files took 1.47 seconds to load)
1 example, 0 failures
Nice. But returning an empty JSON
document is a bit useless. What we want instead is to get the entire list of books
present in the database.
We are going to define a few lets
to avoid repetitions in the tests. If you don’t know what a let
is, it’s an elegant way to define variables for your tests. To give you an idea, defining the following let
…
let(:variable_name) { 'variable_value' }
is the equivalent of something like this:
def variable_name
@variable_name ||= 'variable_value'
end
All let
definitions are wiped between each test. Let’s define one let
for each one of the book factories we created earlier. We are also going to create an array to contain them all, as it will make it easier to create them once we need them.
# spec/requests/books_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
# one let for each book factory. Here we use 'create' instead
# of 'build' because we need the data persisted. Those two methods
# are provided by Factory Girl.
let(:ruby_microscope) { create(:ruby_microscope) }
let(:rails_tutorial) { create(:ruby_on_rails_tutorial) }
let(:agile_web_dev) { create(:agile_web_development) }
# Putting them in an array make it easier to create them in one line
let(:books) { [ruby_microscope, rails_tutorial, agile_web_dev] }
let(:json_body) { JSON.parse(response.body) }
describe 'GET /api/books' do
# Before any test, let's create our 3 books
before { books }
context 'default behavior' do
before { get '/api/books' }
it 'gets HTTP status 200' do
expect(response.status).to eq 200
end
it 'receives a json with the "data" root key' do
expect(json_body['data']).to_not be nil
end
it 'receives all 3 books' do
expect(json_body['data'].size).to eq 3
end
end
end
end
What happens if we run rspec
now? Let’s find out.
rspec spec/requests/books_spec.rb
Failure (RED)
...
Finished in 0.34193 seconds (files took 2.43 seconds to load)
3 examples, 2 failures
...
Of course they fail! We need to update the index
action to make it pass. Luckily, we don’t have much to change, we just need to pass Book.all
as the value for the json
key.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
render json: { data: Book.all }
end
end
One last time.
rspec spec/requests/books_spec.rb
Success (GREEN)
Books
GET /api/books
gets HTTP status 200
receives a json with the 'data' root key
receives all 3 books
Finished in 0.33482 seconds (files took 1.82 seconds to load)
3 examples, 0 failures
Perfect. Let’s check it out in a browser, just to confirm that it’s working with our own eyes.
Start the server with rails s
. Head over to http://localhost:3000/api/books and you should see something looking like this:
{
"data": []
}
There is nothing. That’s because, in order to get something back, we need to have some data stored in the database. I created a small seed file (we will have a bigger one later) that you can copy/paste in db/seeds.rb
.
# db/seeds.rb
pat = Author.create!(given_name: 'Pat', family_name: 'Shaughnessy')
michael = Author.create!(given_name: 'Michael', family_name: 'Hartl')
sam = Author.create!(given_name: 'Sam', family_name: 'Ruby')
oreilly = Publisher.create!(name: "O'Reilly")
Book.create!(title: 'Ruby Under a Microscope',
subtitle: 'An Illustrated Guide to Ruby Internals',
isbn_10: '1593275617',
isbn_13: '9781593275617',
description: 'Ruby Under a Microscope is a cool book!',
released_on: '2013-09-01',
publisher: oreilly,
author: pat)
Book.create!(title: 'Ruby on Rails Tutorial',
subtitle: 'Learn Web Development with Rails',
isbn_10: '0134077709',
isbn_13: '9780134077703',
description: 'The Rails Tutorial is great!',
released_on: '2013-05-09',
publisher: nil,
author: michael)
Book.create!(title: 'Agile Web Development with Rails 4',
subtitle: '',
isbn_10: '1937785564',
isbn_13: '9781937785567',
description: 'Stay agile!',
released_on: '2015-10-11',
publisher: oreilly,
author: sam)
Run rails db:seed
to get those records inserted. Refresh your browser and you should now have a nice list of books appearing as shown in the Figure 1!
As you can see, the books in this representation have all the columns we defined for the books
table as attributes. This is bad for two reasons. First, you really don’t want a tight coupling between what you are sending to a client and how your database looks. You need to be able to change one without changing the other. The second problem is that we have no control over what is being sent back and that’s something we will work on in the next chapter.
In all our request tests, we will be checking the json_body
variable. Creating a small RSpec helper will save us from putting let(:json_body) { JSON.parse(response.body) }
everywhere.
mkdir spec/support && touch spec/support/helpers.rb
Put the following code in it:
# spec/support/helpers.rb
module Helpers
def json_body
JSON.parse(response.body)
end
end
RSpec.configure do |c|
c.include Helpers
end
We can now remove the let(:json_body)
from the book tests.
It’s now time to push our changes to GitHub. The flow is the same as in the previous chapter. But first, let’s run our test suite to ensure that everything is still working.
rspec
Success (GREEN)
Author
should validate that :given_name cannot be empty/falsy
should validate that :family_name cannot be empty/falsy
should have many books
has a valid factory
Book
should validate that :title cannot be empty/falsy
should validate that :released_on cannot be empty/falsy
should validate that :author cannot be empty/falsy
should validate that :isbn_10 cannot be empty/falsy
should validate that :isbn_13 cannot be empty/falsy
should belong to publisher
should belong to author
should validate that the length of :isbn_10 is 10
should validate that the length of :isbn_13 is 13
should validate that :isbn_10 is case-sensitively unique
should validate that :isbn_13 is case-sensitively unique
has a valid factory
Publisher
should validate that :name cannot be empty/falsy
should have many books
has a valid factory
Books
GET /api/books
gets HTTP status 200
receives a json with the 'data' root key
receives all 3 books
Finished in 0.60698 seconds (files took 2.46 seconds to load)
22 examples, 0 failures
Great! Here is the list of steps to push the code.
Check the changes.
git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: config/routes.rb
modified: db/seeds.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/controllers/books_controller.rb
spec/requests/
no changes added to commit (use "git add" and/or "git commit -a")
Stage them.
git add .
Commit the changes.
git commit -m "Add BooksController"
Push to GitHub.
git push origin master
Wow, this chapter was dense - we’ve created so many things! We now have a working books
resource, and we can let the client decide which fields are needed.
By creating the BasePresenter
class, we have also laid out the foundation for the next query builders and representation builders that we are about to create in the next chapter.