For this lesson, we will do part 1 of this tutorial: https://scotch.io/tutorials/build-a-restful-json-api-with-rails-5-part-one . An initial workspace with the starter version of the Rails application is available here.
Introductory Reading
We are going to create an API that communicates using REST protocols, and that exchanges JSON data. It’s a good idea to understand what REST is:
https://dzone.com/articles/introduction-to-rest-api-restful-web-services
And also, you will want to understand JSON:
Creating the API Server Application
The tutorial suggests that you do gem update rails. DO NOT DO THIS. That would take you to Rails 6, which has some complexities that we don’t want to take up at this time. Also, do not do the “rails new” command. Since you are starting from an existing workspace, that has been done for you.
Otherwise, carefully follow the instructions in the tutorial on creating the API in Rails. In completing the tutorial, I found the following corrections were necessary.
First, you must install httpie for use in testing. This is described here: https://httpie.org/ The steps to install it are:
On MacOS: brew install httpie
On Linux (inluding Vagrant linux under WIndows): sudo apt install httpie
Second, before you run Rspec, but after you do bin/rails db:migrate, you will need to do
bin/rails db:migrate RAILS_ENV=test
Third, at one point in the instructions, there is the following typo:
RSpec.configuration do |config|
It should be
RSpec.configure do |config|
To run Rspec, you should do:
bundle exec rspec
There are also some problems with the descriptions of the Todo and Item factories as described in the tutorial. The factory files should actually read as follows:
# spec/factories/todos.rb
FactoryBot.define do
factory :todo do
title { Faker::Lorem.word }
created_by { Faker::Number.number(digits: 10) }
end
end
# spec/factories/items.rb
FactoryBot.define do
factory :item do
name { Faker::Movies::StarWars.character }
done { false }
todo_id { nil }
end
end
When you are finished with all of the steps in the tutorial, be sure you use httpie to test all of the operations, and using httpie create a few todos, and a few items belonging to each todo.
Adding Swagger
Swagger is a framework to document and test REST APIs. The following instructions describe how you add Swagger enablement to your API application.
First, add this line to the Gemfile:
gem ‘rswag’
Be sure you add it to the section before the development, test section in the Gemfile. Then do a bundle install. Then do
bin/rails g rswag:install
Swagger pages are defined using a YAML file, which is automatically generated from your Rspec test files — but only for test items that are of a certain format. Therefore, you must add the following lines to spec/requests/todos_spec.rb, just before the final end:
path '/todos' do
get('list todos') do
tags 'Todos'
response(200, 'successful') do
after do |example|
example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) }
end
run_test!
end
end
post('create todo') do
tags 'Todos'
consumes 'application/json'
produces 'application/json'
parameter name: :todo, in: :body, required: true, schema: {
type: :object,
required: %i[title created_by],
properties: {
title: { type: :string },
created_by: { type: :string }
}
}
response(201, 'successful') do
let(:todo) { { title: 'Learn Elm', created_by: '1' } }
run_test!
end
end
end
path '/todos/{id}' do
parameter name: 'id', in: :path, type: :integer, description: 'id'
get('show todo') do
tags 'Todos'
response(200, 'successful') do
let(:id) { 5 }
# after do |example|
# example.metadata[:response][:examples] = { 'application/json' => JSON.parse(response.body, symbolize_names: true) }
# end
run_test!
end
end
put('update todo') do
tags 'Todos'
parameter name: :todo, in: :body, schema: {
type: :object,
properties: {
title: { type: :string },
content: { type: :string }
}
}
response(204, "successful") do
let(:id) { 5 }
run_test!
end
end
delete('delete todo') do
tags 'Todos'
response(204, "successful") do
let(:id) { 5 }
run_test!
end
end
end
Also, add these lines to spec/requests/items_spec.rb, again just before the final end:
path '/todos/{todo_id}/items' do
parameter name: 'todo_id', in: :path, type: :integer, description: 'todo_id'
get('list items') do
tags 'Items'
response(200, 'success') do
run_test!
end
end
post('create item') do
tags 'Items'
consumes 'application/json'
produces 'application/json'
parameter name: :item, in: :body, required: true, schema: {
type: :object,
required: %i[name],
properties: {
name: {type: :string},
done: {type: :boolean}
}
}
response(201, 'success') do
let(:item) { { name: 'thisitem', done: false } }
run_test!
end
end
end
path '/todos/{todo_id}/items/{id}' do
parameter name: 'todo_id', in: :path, type: :integer, description: 'todo_id'
parameter name: 'id', in: :path, type: :integer, description: 'id'
get('show item') do
tags 'Items'
response(200,'success') do
run_test!
end
end
put('update item') do
tags 'Items'
consumes 'application/json'
produces 'application/json'
parameter name: :item, in: :body, required: true, schema: {
type: :object,
properties: {
name: {type: :string},
done: {type: :boolean}
}
}
response(204,'success') do
let(:item) { { name: 'changedName'} }
run_test!
end
end
delete('delete item') do
tags 'Items'
response(204,'success') do
run_test!
end
end
end
Finally, edit spec/swagger_helper.rb so that the servers section reads as follows:
servers: [
{
url: "#{ENV['APPLICATION_URL']}"
}
]
When you get all done with this, run
bundle exec rspec
To make sure that the test suites still work. Then type
bundle exec rake rswag:specs:swaggerize
Then start your server as usual. You will find that you have a new route, so that you can, from your browser, access http://localhost:3000/api-docs . Experiment with this page, trying out the various APIs and creating and updating todo and item entries.
AJAX
Now we will call the API using AJAX, from a front end application. There are several steps needed to get ready.
First, for those running Vagrant, you will have to edit your Vagrantfile, add a line at the bottom just before the final end, and restart Vagrant. You exit your vagrant ssh session and then do a vagrant halt, followed by a vagrant up after the Vagrantfile has been edited. The line you add is:
config.vm.network :forwarded_port, guest: 3001, host: 3001, host_ip: “127.0.0.1”
Second, we need to add an additional gem to our todos-api application, and change a configuration file. This is for security reasons having to do with something called CORS, or cross origin resource sharing. You add this line to your Gemfile:
gem ‘rack-cors’
Add it above the development, test section. Then you do bundle install. Then you add the following lines to config/application.rb:
config.middleware.insert_before 0, "Rack::Cors" do
allow do
origins 'http://localhost:3000'
resource(
'*',
headers: :any,
methods: [:get, :patch, :put, :delete, :post, :options]
)
end
end
These should be added just before the second to the last end statement in the file.
Now, you clone the following workspace for the ajax-sample application here. This sample application is fully functional but limited. Now you start BOTH applications. For todos-api, you start it as follows:
bin/rails s -p 3001
Or, if you are running vagrant:
bin/rails s -b 0.0.0. 0 -p 3001
For ajax-sample, you let it default to use port 3000:
bin/rails s
or if you are running vagrant:
bin/rails s -b 0.0.0.0
Now open the browser to localhost:3000. You will see a very basic page, On one line is a pushbutton to list the todos. If you push the button, an AJAX request is sent to retrieve the todo entries from the API application, and it comes back in JSON (looking kind of ugly). On another line is two entry fields and another pushbutton to create a todo. If you fill out the fields and push the the second button, it will create a todo and show the todo it created, again in JSON.
Study the javascript in the ajax-sample file app/views/ajax/home.html.erb . You will see that it creates a click event listener for the button, and then when it is clicked, it generates an XMLHttpRequest (AJAX), which causes the list of todos to be retrieved. Further, you will see an event listener for the second button. You will see that it also generates an Ajax request, this one to create a new todo. This second one posts a JSON body.
AJAX Assignment
Extend the home.html.erb file. Add an entry field and a second pushbutton for list items.. The entry field should be for the id of a todo entry. When the second pushbutton is clicked, it should retrieve all the items belonging to that todo entry, via an AJAX request. You will have to send the AJAX request to the URL http://localhost:3000/todos/todo_id/items where todo_id is the id from the entry field.
Once you get this working, extend the home.html.erb file further. Add entry fields for name and todo_id, and a checkbox for the boolean done, as well as a pushbutton for create item. You want to create an item belonging to the todo in the todo_id. You will need to send a POST request to the same URL as for the item list request, with a JSON encoded body for the item object to be created. Note that the todo_id is not in the JSON body because it is in the URL.
This reference may be helpful: https://www.sitepoint.com/guide-vanilla-ajax-without-jquery/