In the previous chapters, we built a set of classes to help us create representations based on the client’s needs. Now that they are all ready to serve a greater purpose, we can switch our attention back to our controllers.
We currently only have one action implemented on the books
controller, and we haven’t even created the authors
and publishers
controllers. Well, the wait is over. By the end of this chapter, we will have all those controllers ready.
Let’s get started.
The books
controller, like any good Rails controller, will need to handle the basic functions of listing, showing, creating, updating and deleting books.
In this section, we are going to add actions to the books
controller to handle these various behavior one at a time. Then we’ll go through the authors
and publishers
controller quickly, since the logic is going to be exactly the same.
We’ve built the index
action that responds to the /api/books
URI in the past chapters. There’s not much to change in it. For reference, here is the current code of the BooksController
class.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
books = orchestrate_query(Book.all)
serializer = Alexandria::Serializer.new(data: books,
params: params,
actions: [:fields, :embeds])
render json: serializer.to_json
end
end
The way we use the Alexandria::Serializer
is going to be shared with the other actions we are about to define. The only thing we are going to change in the index
action is extracting the serialization part in the ApplicationController
class.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
rescue_from QueryBuilderError, with: :builder_error
rescue_from RepresentationBuilderError, with: :builder_error
protected
def builder_error # Hidden Code
def orchestrate_query # Hidden Code
def serialize(data)
{
json: Alexandria::Serializer.new(data: data,
params: params,
actions: [:fields, :embeds]).to_json
}
end
end
Then, we can just use this new method in the books
controller.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
books = orchestrate_query(Book.all)
render serialize(books)
end
end
Two lines of code. That’s pretty clean. But did we break anything?
rspec
Success (GREEN)
...
Finished in 2.99 seconds (files took 2.53 seconds to load)
82 examples, 0 failures
With the index
action fully ready, let’s move on to the show
action associated with the /api/books/:id
URI template.
First, we need to define our expectations by writing a few automated tests.
The show
action is simple and has two possible outcomes:
404
.
Here are the tests matching those expectations.
# spec/requests/books_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
let(:ruby_microscope) { create(:ruby_microscope) }
let(:rails_tutorial) { create(:ruby_on_rails_tutorial) }
let(:agile_web_dev) { create(:agile_web_development) }
let(:books) { [ruby_microscope, rails_tutorial, agile_web_dev] }
describe 'GET /api/books' # Hidden Code
describe 'GET /api/books/:id' do
context 'with existing resource' do
before { get "/api/books/#{rails_tutorial.id}" }
it 'gets HTTP status 200' do
expect(response.status).to eq 200
end
it 'receives the "rails_tutorial" book as JSON' do
expected = { data: BookPresenter.new(rails_tutorial, {}).fields.embeds }
expect(response.body).to eq(expected.to_json)
end
end
context 'with nonexistent resource' do
it 'gets HTTP status 404' do
get '/api/books/2314323'
expect(response.status).to eq 404
end
end
end # describe 'GET /api/books/:id'
end
Try to run them.
rspec spec/requests/books_spec.rb
Failure (RED)
...
Finished in 1.86 seconds (files took 2.5 seconds to load)
31 examples, 3 failures
...
Alright, failing! Let’s fix them.
Since the “resource not found” behavior will be needed for more than the show
action, we are going to extract its logic and define it as a before_action
filter. But first, we have to define the resource_not_found
method in the ApplicationController
that will halt the request and return 404
unless the resource is found.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
rescue_from QueryBuilderError, with: :builder_error
rescue_from RepresentationBuilderError, with: :builder_error
rescue_from ActiveRecord::RecordNotFound, with: :resource_not_found
protected
def builder_error # Hidden Code
def orchestrate_query # Hidden Code
def serialize # Hidden Code
def resource_not_found
render(status: 404)
end
end
In each of our controllers, we will define a method like the one below that will return the current entity being accessed or instantiate a new one based on the passed parameters.
Note that we use find_by!
to raise an ActiveRecord::RecordNotFound
exception that will be caught by the ApplicationController
.
def book
@book ||= params[:id] ? Book.find_by!(id: params[:id]) : Book.new(book_params)
end
Next to it, we will also define an alias as resource
that will be used later on.
def book
@book ||= params[:id] ? Book.find_by!(id: params[:id]) : Book.new(book_params)
end
alias_method :resource, :book
Defining those methods allows us to remove duplicated code from our actions and keep only the bare minimum. Our show
action, for example, will be composed of only one line of code.
def show
render serialize(book)
end
See how it all comes together in the books
controller.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index
books = orchestrate_query(Book.all)
render serialize(books)
end
def show
render serialize(book)
end
private
def book
@book ||= params[:id] ? Book.find_by!(id: params[:id]) : Book.new(book_params)
end
alias_method :resource, :book
end
Let’s try to run the tests to see if our expectations have been met by our implementation.
rspec spec/requests/books_spec.rb
Success (GREEN)
...
Finished in 1.41 seconds (files took 2.84 seconds to load)
31 examples, 0 failures
The show
action is ready!
Retrieving books is only possible if there are books to retrieve. With this in mind, we have to implement an action (create
) for the /api/books
URI associated with the POST
HTTP method.
To test this URI, we need to write more tests. Since the create
action receives parameters from the client, we need to test two different contexts: when the client sends valid data and when it does not. Let’s focus on the former first.
# spec/requests/books_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
# Hidden Code
describe 'GET /api/books' do # Hidden Code
describe 'GET /api/books/:id' do # Hidden Code
describe 'POST /api/books' do
let(:author) { create(:michael_hartl) }
before { post '/api/books', params: { data: params } }
context 'with valid parameters' do
let(:params) do
attributes_for(:ruby_on_rails_tutorial, author_id: author.id)
end
it 'gets HTTP status 201' do
expect(response.status).to eq 201
end
it 'receives the newly created resource' do
expect(json_body['data']['title']).to eq 'Ruby on Rails Tutorial'
end
it 'adds a record in the database' do
expect(Book.count).to eq 1
end
it 'gets the new resource location in the Location header' do
expect(response.headers['Location']).to eq(
"http://www.example.com/api/books/#{Book.first.id}"
)
end
end
end # describe 'POST /api/books'
end
Try to run the tests.
rspec spec/requests/books_spec.rb
Failure (RED)
...
Finished in 2 seconds (files took 2.33 seconds to load)
35 examples, 4 failures
...
To make those tests pass, we need to implement the create
action with the default behavior, when everything goes well and the client sends parameters that will allow the entity to be saved.
The create
action is going to use the strong parameters feature of Ruby on Rails to ensure that only allowed parameters go through.
def book_params
params.require(:data).permit(:title, :subtitle, :isbn_10, :isbn_13,
:description, :released_on, :publisher_id,
:author_id, :cover)
end
With those filtered parameters, we can then create a new book and return it to the client with a 201
status and the location of the URI of the new resource in the Location
header as specified in the HTTP RFC.
def create
book = Book.create(book_params)
render serialize(book).merge(status: :created, location: book)
end
It all comes together in the books
controller like this.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index # Hidden Code
def show # Hidden Code
def create
book = Book.create(book_params)
render serialize(book).merge(status: :created, location: book)
end
private
def book # Hidden Code
alias_method :resource, :book
def book_params
params.require(:data).permit(:title, :subtitle, :isbn_10, :isbn_13,
:description, :released_on, :publisher_id,
:author_id, :cover)
end
end
With this code, our tests are now working.
rspec spec/requests/books_spec.rb
Success (GREEN)
...
Finished in 1.62 seconds (files took 1.86 seconds to load)
35 examples, 0 failures
But now we need to think about clients sending invalid parameters which would prevent the entities from being saved in the database - for example, not giving a title
to a book.
Let’s write some tests for this context where the request sent by the client contains invalid parameters.
# spec/requests/books_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
# Hidden Code
describe 'GET /api/books' do # Hidden Code
describe 'GET /api/books/:id' do # Hidden Code
describe 'POST /api/books' do
let(:author) { create(:michael_hartl) }
before { post '/api/books', params: { data: params } }
context 'with valid parameters' # Hidden Code
context 'with invalid parameters' do
let(:params) { attributes_for(:ruby_on_rails_tutorial, title: '') }
it 'gets HTTP status 422' do
expect(response.status).to eq 422
end
it 'receives the error details' do
expect(json_body['error']['invalid_params']).to eq(
{'author'=>['must exist', "can't be blank"], 'title'=>["can't be blank"]}
)
end
it 'does not add a record in the database' do
expect(Book.count).to eq 0
end
end # context 'with invalid parameters'
end
end
rspec spec/requests/books_spec.rb
Failure (RED)
...
Finished in 1.42 seconds (files took 2.45 seconds to load)
38 examples, 2 failures
...
When the server receives invalid parameters like this, it should send back a 422 Unprocessable Entity
status code. Since we will be using this behavior for other controllers and actions (update
), we are going to add a method in ApplicationController
to deal with it.
You can see the unprocessable_entity!
method below. All it does is send a JSON
document back to the client with the 422
status code.
The invalid_params
key contains the errors that prevent the entity from being saved.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
rescue_from QueryBuilderError, with: :builder_error
rescue_from RepresentationBuilderError, with: :builder_error
rescue_from ActiveRecord::RecordNotFound, with: :resource_not_found
protected
def builder_error # Hidden Code
def unprocessable_entity!(resource)
render status: :unprocessable_entity, json: {
error: {
message: "Invalid parameters for resource #{resource.class}.",
invalid_params: resource.errors
}
}
end
def serialize # Hidden Code
def orchestrate_query # Hidden Code
def resource_not_found # Hidden Code
end
We just have to change the logic a tiny bit to return a 422 Unprocessable Entity
status to the client. To achieve this, we need to add a check to see if the book can be saved or not. We can now use the book
method we created earlier to build the book for us.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index # Hidden Code
def show # Hidden Code
def create
if book.save
render serialize(book).merge(status: :created, location: book)
else
unprocessable_entity!(book)
end
end
private
def book # Hidden Code
def book_params
params.require(:data).permit(:title, :subtitle, :isbn_10, :isbn_13,
:description, :released_on, :publisher_id,
:author_id, :cover)
end
end
Run the tests. They should all pass without any issues.
rspec spec/requests/books_spec.rb
Success (GREEN)
...
Finished in 1.7 seconds (files took 2.22 seconds to load)
38 examples, 0 failures
We don’t want to allow entire entities to be replaced. Instead, we prefer to only allow partial changes using the PATCH
method.
Implementing the update
action that responds to PATCH /api/books/:id
is going to be very similar to the create
action. We are using the same contexts actually.
Here are the tests for it.
# spec/requests/books_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
# Hidden Code
describe 'GET /api/books' # Hidden Code
describe 'GET /api/books/:id' # Hidden Code
describe 'POST /api/books' # Hidden Code
describe 'PATCH /api/books/:id' do
before { patch "/api/books/#{rails_tutorial.id}", params: { data: params } }
context 'with valid parameters' do
let(:params) { { title: 'The Ruby on Rails Tutorial' } }
it 'gets HTTP status 200' do
expect(response.status).to eq 200
end
it 'receives the updated resource' do
expect(json_body['data']['title']).to eq(
'The Ruby on Rails Tutorial'
)
end
it 'updates the record in the database' do
expect(Book.first.title).to eq 'The Ruby on Rails Tutorial'
end
end
context 'with invalid parameters' do
let(:params) { { title: '' } }
it 'gets HTTP status 422' do
expect(response.status).to eq 422
end
it 'receives the error details' do
expect(json_body['error']['invalid_params']).to eq(
{ 'title'=>["can't be blank"] }
)
end
it 'does not add a record in the database' do
expect(Book.first.title).to eq 'Ruby on Rails Tutorial'
end
end
end # describe 'PATCH /api/books/:id' end
end
Run the tests to see them fail.
rspec spec/requests/books_spec.rb
Failure (RED)
...
Finished in 2.41 seconds (files took 3 seconds to load)
44 examples, 6 failures
Here is the implementation in the books
controller. There’s nothing much to say. We’ll try to update the specified resource, and if it fails we will return a 422 Unprocessable Entity
error. If it succeeds, we can return the serialized book.
Here is the complete controller with the update
action.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index # Hidden Code
def show # Hidden Code
def create # Hidden Code
def update
if book.update(book_params)
render serialize(book).merge(status: :ok)
else
unprocessable_entity!(book)
end
end
private
def book # Hidden Code
def book_params # Hidden Code
end
Finally, run the tests to ensure that everything is working properly.
rspec spec/requests/books_spec.rb
Success (GREEN)
...
Finished in 1.9 seconds (files took 3.07 seconds to load)
44 examples, 0 failures
We have two ways to implement the deletion of entities from the database. First, we can stick to the HTTP RFC and always return 204 No Content
, even if the resource has already been deleted.
def destroy
book = Book.where(id: params[:id]).first
book.destroy if book
render status: :no_content
end
The second option is more common. The idea is that trying to delete a nonexistent resource should result in a 404 Not Found
status code.
def destroy
Book.find_by!(id: params[:id]).destroy
render status: :no_content
end
For this API, I’ve decided (arbitrarily) to go with the second option, so let’s write the tests for it.
# spec/requests/books_spec.rb
require 'rails_helper'
RSpec.describe 'Books', type: :request do
# Hidden Code
describe 'GET /api/books' # Hidden Code
describe 'GET /api/books/:id' # Hidden Code
describe 'POST /api/books' # Hidden Code
describe 'PATCH /api/books/:id' # Hidden Code
describe 'DELETE /api/books/:id' do
context 'with existing resource' do
before { delete "/api/books/#{rails_tutorial.id}" }
it 'gets HTTP status 204' do
expect(response.status).to eq 204
end
it 'deletes the book from the database' do
expect(Book.count).to eq 0
end
end
context 'with nonexistent resource' do
it 'gets HTTP status 404' do
delete '/api/books/2314323'
expect(response.status).to eq 404
end
end
end # describe 'DELETE /api/books/:id' end
end
Run the tests.
rspec spec/requests/books_spec.rb
Failure (RED)
...
Finished in 1.48 seconds (files took 2.37 seconds to load)
47 examples, 3 failures
...
Only two things to change. First, we need to run resource_not_found
before the destroy
action, so let’s change the list of actions to [:show, :update, :destroy]
. The second change is the destroy
action itself, which we’ve already talked about. There is nothing else to add here - the two lines composing it are pretty simple.
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def index # Hidden Code
def show # Hidden Code
def create # Hidden Code
def update # Hidden Code
def destroy
book.destroy
render status: :no_content
end
private
def book # Hidden Code
def book_params # Hidden Code
end
Run the tests to check that they are all passing.
rspec spec/requests/books_spec.rb
Success (GREEN)
...
Finished in 1.57 seconds (files took 2.94 seconds to load)
47 examples, 0 failures
Thanks to our preparation in the past chapters, the authors
and publishers
controllers are going to be super simple to implement. I’m actually not going to give you the tests this time - I’d like you to try writing them instead.
You don’t have to test everything but if you decide to write some tests, that would be awesome! I’m still going to give you the authors_spec
file with the main context and you should be able to fill them as you see fit.
Note that if you purchased the medium or complete package, you have all the tests in the solutions folder.
First, let’s create the files.
touch app/controllers/authors_controller.rb \
spec/requests/authors_spec.rb
We also need to add the routes or else the Rails router won’t match the URIs with our controller.
# config/routes.rb
Rails.application.routes.draw do
scope :api do
resources :books
resources :authors
end
end
And now, well, it’s time for you to write some tests ;). Below you will find the authors_spec
file with all the contexts
you need. These follow the same logic we implemented for the books
controller tests, so don’t expect anything new. You just need to adapt the logic to make it work for authors
. I’ve already prepared a bunch of let
to help you get started.
# spec/requests/authors_spec.rb
require 'rails_helper'
RSpec.describe 'Authors', type: :request do
let(:pat) { create(:author) }
let(:michael) { create(:michael_hartl) }
let(:sam) { create(:sam_ruby) }
let(:authors) { [pat, michael, sam] }
describe 'GET /api/authors' do
context 'default behavior' do
end
describe 'field picking' do
context 'with the fields parameter' do
end
context 'without the field parameter' do
end
context 'with invalid field name "fid"' do
end
end
describe 'pagination' do
context 'when asking for the first page' do
end
context 'when asking for the second page' do
end
context 'when sending invalid "page" and "per" parameters' do
end
end
describe 'sorting' do
context 'with valid column name "id"' do
end
context 'with invalid column name "fid"' do
end
end
describe 'filtering' do
context 'with valid filtering param "q[given_name_cont]=Pat"' do
end
context 'with invalid filtering param "q[fgiven_name_cont]=Pat"' do
end
end
end
describe 'GET /api/authors/:id' do
context 'with existing resource' do
end
context 'with nonexistent resource' do
end
end
describe 'POST /api/authors' do
context 'with valid parameters' do
end
context 'with invalid parameters' do
end
end
describe 'PATCH /api/authors/:id' do
context 'with valid parameters' do
end
context 'with invalid parameters' do
end
end
describe 'DELETE /api/authors/:id' do
context 'with existing resource' do
end
context 'with nonexistent resource' do
end
end
end
Now let’s see the AuthorsController
implementation. There shouldn’t be anything surprising in there, as it works exactly like the books
controller. We can even create an intermediary controller from which BooksController
, AuthorsController
and PublishersController
could inherit. I’ll let you do that if you want to - it’s not really complicated.
# app/controllers/authors_controller.rb
class AuthorsController < ApplicationController
def index
authors = orchestrate_query(Author.all)
render serialize(authors)
end
def show
render serialize(author)
end
def create
if author.save
render serialize(author).merge(status: :created, location: author)
else
unprocessable_entity!(author)
end
end
def update
if author.update(author_params)
render serialize(author).merge(status: :ok)
else
unprocessable_entity!(author)
end
end
def destroy
author.destroy
render status: :no_content
end
private
def author
@author ||= params[:id] ? Author.find_by!(id: params[:id]) :
Author.new(author_params)
end
alias_method :resource, :author
def author_params
params.require(:data).permit(:given_name, :family_name)
end
end
If you wrote the tests, run them to check that everything is working.
rspec spec/requests/authors_spec.rb
Success (GREEN)
...
Finished in 1.09 seconds (files took 2.76 seconds to load)
43 examples, 0 failures
Implementing the publishers
controller is going to be exactly like the authors
controller. That’s why we will go through this section pretty fast.
First, create the files.
touch app/controllers/publishers_controller.rb \
spec/requests/publishers_spec.rb
Add the publishers
routes.
# config/routes.rb
Rails.application.routes.draw do
scope :api do
resources :books
resources :authors
resources :publishers
end
end
To write some meaningful tests for the publishers
controller, let’s add more factories.
# spec/factories/publishers.rb
FactoryBot.define do
factory :publisher do
name { "O'Reilly" }
end
factory :dev_media, class: Publisher do
name { 'Dev Media' }
end
factory :super_books, class: Publisher do
name { 'Super Books' }
end
end
Now, it’s your time to shine (again). Write tests for the publishers
controller. Here is a skeleton to get you started.
# spec/requests/publishers_spec.rb
require 'rails_helper'
RSpec.describe 'Publishers', type: :request do
let(:oreilly) { create(:publisher) }
let(:dev_media) { create(:dev_media) }
let(:super_books) { create(:super_books) }
let(:publishers) { [oreilly, dev_media, super_books] }
describe 'GET /api/publishers' do
context 'default behavior' do
end
describe 'field picking' do
context 'with the fields parameter' do
end
context 'without the field parameter' do
end
context 'with invalid field name "fid"' do
end
end
describe 'pagination' do
context 'when asking for the first page' do
end
context 'when asking for the second page' do
end
context 'when sending invalid "page" and "per" parameters' do
end
end
describe 'sorting' do
context 'with valid column name "id"' do
end
context 'with invalid column name "fid"' do
end
end
describe 'filtering' do
context 'with valid filtering param "q[name_cont]=Reilly"' do
end
context 'with invalid filtering param "q[fname_cont]=Reilly"' do
end
end
end
describe 'GET /api/publishers/:id' do
context 'with existing resource' do
end
context 'with nonexistent resource' do
end
end
describe 'POST /api/publishers' do
context 'with valid parameters' do
end
context 'with invalid parameters' do
end
end
describe 'PATCH /api/publishers/:id' do
context 'with valid parameters' do
end
context 'with invalid parameters' do
end
end
describe 'DELETE /api/publishers/:id' do
context 'with existing resource' do
end
context 'with nonexistent resource' do
end
end
end
And here is the PublishersController
implementation.
# app/controllers/publishers_controller.rb
class PublishersController < ApplicationController
def index
publishers = orchestrate_query(Publisher.all)
render serialize(publishers)
end
def show
render serialize(publisher)
end
def create
publisher = Publisher.new(publisher_params)
if publisher.save
render serialize(publisher).merge(status: :created, location: publisher)
else
unprocessable_entity!(publisher)
end
end
def update
if publisher.update(publisher_params)
render serialize(publisher).merge(status: :ok)
else
unprocessable_entity!(publisher)
end
end
def destroy
publisher.destroy
render status: :no_content
end
private
def publisher
@publisher ||= params[:id] ? Publisher.find_by!(id: params[:id]) :
Publisher.new(publisher_params)
end
alias_method :resource, :publisher
def publisher_params
params.require(:data).permit(:name)
end
end
If you wrote the tests, run them to check that everything is working.
rspec spec/requests/publishers_spec.rb
Success (GREEN)
Finished in 0.88656 seconds (files took 3.43 seconds to load)
43 examples, 0 failures
And that’s it! We now have three functional controllers in our API. Well done!
We can now create or update books. In an earlier chapter, we defined a process to upload pictures from anywhere without using a form; this was to make it easier for different clients to upload images. Now, it’s time to test our implementation.
Receiving images encoded in base64 means we will have huge strings of characters in the parameters. That’s not a problem, but I’d rather not have the logs spammed with those base64 encoded images. To avoid that, we just need to add the cover
parameter to the list of parameters that must be filtered in the logs, just like the passwords.
To do this, add config.filter_parameters += [:cover]
to the config/application.rb
file.
# config/application.rb
require_relative 'boot'
require "rails"
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
require "rails/test_unit/railtie"
require 'carrierwave'
require 'carrierwave/orm/activerecord'
Bundler.require(*Rails.groups)
module Alexandria
class Application < Rails::Application
config.load_defaults 5.2
config.api_only = true
config.filter_parameters += [:cover]
end
end
We can use curl
to upload a cover to a book. With the book, you should have received a folder named images
in the resources
archive. Copy this folder into your application folder and run the following curl
request from inside your API folder to upload the cover image.
Start the server with rails s
if it’s not already running.
Note that, when using carrierwave-base64
, the base64 string needs to start with data:image/jpg;base64
.
(echo -n '{"data":{"cover": "data:image/jpeg;base64,'; \
base64 images/cover.jpg; echo '"}}') |
curl -X PATCH -H "Content-Type: application/json" -d @- \
http://localhost:3000/api/books/1
Output
{
"data":{
"id":1,
"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_id":1,"author_id":1,
"created_at":"2016-06-04T16:45:02.057Z",
"updated_at":"2016-06-06T09:31:28.969Z",
"cover":"/uploads/book/cover/1/cover.jpeg"
}
}
If we checkout the cover path appended to the base url manually in a browser, we get the cover we just uploaded back as seen in Figure 1.
It’s working! But we shouldn’t be satisfied just yet. Instead of only giving the path of the cover to the client, why not take one more step toward HATEOAS and give full URLs?
We can use the Rails.application.routes.url_helpers
module to get the root URL. Include it in the BasePresenter
class:
# app/presenters/base_presenter.rb
class BasePresenter
include Rails.application.routes.url_helpers
# Hidden Code
end
To be able to use the root_url
method provided by Rails, we need to define the root
in the routes files.
# config/routes.rb
Rails.application.routes.draw do
scope :api do
resources :books
resources :authors
resources :publishers
end
root to: 'books#index'
end
We also need to add some configuration defining what the base host URL is in the test
and development
environments.
# config/environments/test.rb
Rails.application.configure do
# Hidden Code
default_url_options[:host] = 'localhost:3000'
end
# config/environments/development.rb
Rails.application.configure do
# Hidden Code
default_url_options[:host] = 'localhost:3000'
end
Restart the server with CTRL-C
and rails s
to get these changes loaded.
Next, we can update the cover
method in the book presenter to use the root_url
.
# app/presenters/book_presenter.rb
class BookPresenter < BasePresenter
build_with :id, :title, :subtitle, :isbn_10, :isbn_13, :description,
:released_on, :publisher_id, :author_id, :created_at, :updated_at,
:cover
related_to :publisher, :author
sort_by :id, :title, :released_on, :created_at, :updated_at
filter_by :id, :title, :isbn_10, :isbn_13, :released_on, :publisher_id,
:author_id
def cover
path = @object.cover.url.to_s
path[0] = '' if path[0] == '/'
"#{root_url}#{path}"
end
end
Let’s see how the representation looks like now.
curl http://localhost:3000/api/books/1
{
"data":{
"id":1,
"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_id":1,
"author_id":1,
"created_at":"2016-06-04T16:45:02.057Z",
"updated_at":"2016-06-06T09:31:28.969Z",
"cover":"http://localhost:3000/uploads/book/cover/1/file.jpg"
}
}
We are now getting the full URL which makes it easier for our clients, and removes the need for them to build the URL themselves.
Let’s not forget to add /public/uploads
to the .gitignore
file to avoid having our uploads being added to Git. Let’s also include the images/
folder we moved inside the project that only contains our sample cover; we don’t need to version that.
# Ignore bundler config.
/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore Byebug command history file.
.byebug_history
/public/uploads
/images
Let’s run the tests first before pushing our changes.
rspec
Success (GREEN)
...
Finished in 6.35 seconds (files took 2.23 seconds to load)
189 examples, 0 failures
Great! Here is the list of steps to push the code.
Check the changes.
git status
Output
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: app/controllers/application_controller.rb
modified: app/controllers/books_controller.rb
modified: config/routes.rb
modified: spec/factories/publishers.rb
modified: spec/requests/books_spec.rb
modified: app/presenters/base_presenter.rb
modified: app/presenters/book_presenter.rb
modified: config/application.rb
modified: config/environments/development.rb
modified: config/environments/test.rb
Untracked files:
(use "git add <file>..." to include in what will be committed)
app/controllers/authors_controller.rb
app/controllers/publishers_controller.rb
spec/requests/authors_spec.rb
spec/requests/publishers_spec.rb
no changes added to commit (use "git add" and/or "git commit -a")
Stage them.
git add .
Commit the changes.
git commit -m "Implement Books, Authors and Publishers controllers"
Output
[master 88a387b] Implement Books, Authors and Publishers controllers
15 files changed, 916 insertions(+), 14 deletions(-)
create mode 100644 app/controllers/authors_controller.rb
rewrite app/controllers/books_controller.rb (60%)
create mode 100644 app/controllers/publishers_controller.rb
create mode 100644 spec/requests/authors_spec.rb
create mode 100644 spec/requests/publishers_spec.rb
Push to GitHub.
git push origin master
In this chapter, we learned how to implement controllers using the tools we built in the previous chapters.
In the next chapter, we will implement a feature that any e-commerce website needs: a way to search for any product using any string of characters. Essentially, we’ll implement full-text search.