Chapter 14

Exposing the Books Resource

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!

14.1. Creating the Controller

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!

14.2. The Index Action

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.

14.3. Returning Books

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.

14.4. Seeding Some Data

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!

https://s3.amazonaws.com/devblast-mrwa-book/images/figures/14/seeded
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.

14.5. Adding a RSpec Helper

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.

14.6. Pushing Our Changes

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

14.7. Wrap Up

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.