Building plain Rails 5 API (Part 1)
Almost 95% of apps written in ruby today, as of 2017, are built using rails as the framework. It is not surprising, as Rails helped propel ruby into the mainstream developer community. 10 years on, Rails has now become a mature framework, battle-tested in lots of production environments. It is common knowledege that performance of rails can be labelled as sluggish in comparison to the newer languages and frameworks, a blemish that is also shouldered by the ruby language itself. So the general sentiment when you want to build an API backend is to never choose rails. However, when performance is not high on priority and when speed of delivery is more important, this is the best option for any ruby or Rails developer.
When developing an API using ruby, you have several viable options which are Sinatra and Grape. Those two alternatives are not a bad shout. Sinatra is the second most popular ruby framework after Rails (though in terms of popularity is not even close to Rails), and after working on it, it is easy to appreciate the simplicity it brings. The fact that it comes in small package and very minimalistic is the polar opposite of Rails, and this is why people like to use Sinatra to build API or microservice. Grape, though, is a ruby framework that is specialized for API development since 2010. So it has good track-record and community of maintainers and users that make it a safe choice to use in production, ensuring that any problem you may have, other people would have also experienced it before. It comes with everything you need to build API, from params filtering, versioning, and serializing response from endpoints. However, when building an API using these two, you will not have some of the magic provided by Rails for free. You will have to add it on your own.
Heck, even you do not know ruby or Rails, developing API using Rails will get you faster development time just because Rails provide you a lot of things out of the gate. Simple things that we, Rails developer, take for granted, like development.rb, test.rb, and production.rb, are actually very helpful in developing, testing, and running the API in production. As Rails is now a mature framework with lots of apps already in production for long time, you can be sure that the production.rb gives you optimized and safe configuration even if you do not understand or tweak a single line at all. Well, I personally suggest you to just at least tweak one line, which is to turn on the forcing of SSL in production for every request made to the API. But apart from that, you are good to go in production. Not to mention that Rails 5, even in API mode, comes packaged with ActiveRecord, this means dealing with database, retrieving and serving data from it will be easy.
You may then opt to use Grape inside of a Rails app to just build the API. Using this approach, you do get the best of the two worlds. You get the free configurations made by Rails and the features of Grape to build the API easily. However you will still need to be familliar with the DSL of Grape and read its documentation thoroughly. In actuality, developing some functionalities provided by Grape, such as filtering parameteres, in Rails is not that hard. The goal of this post is to convince you to write plain Rails API and in the next part, I will start giving you examples. Because requirement in developing API can be varied, I will write about this in a number of serialized part. This first part will be about writing a simple DRY API-only app using Rails 5, though you can also use it to build an API inside a full-blown Rails app.
Setting Up
rails new sample_api --api --database=postgresql
Below here is all the additional gems that we are going to install for this post. Leave the default gems provided by the generator alone.
Gemfile
gem 'figaro'
group :test do
gem 'capybara'
gem 'minitest-reporters'
gem 'shoulda-context'
gem 'shoulda-matchers'
end
This is important, let's uncomment a line of config to ensure that we use SSL in production environment.
config/environments/production.rb
config.force_ssl = true
Now every request made to the API in production will be forced to use https, even if the requester does not type https. The next step is to install figaro so we can store sensitive environment variables. Though this is optional, it is very recommended so you do not end up hardcoding any environment variable in your app. Apart from Figaro, another alternative is to use Dotenv, which is similar, but I prefer Figaro's simplicity.
bundle exec figaro install
That command automatically generates a file in the config folder called 'application.yml' that will store all the env variables and also add this file to our .gitignore file. This means that the application.yml will not be commited to the source, unless of course, you remove that line from .gitignore. Copy all the secret keys that you have on config/secrets.yml and put them inside the application.yml
config/application.yml
development:
DATABASE_NAME: 'your-development-database'
DATABASE_USER: 'your-database-user'
DATABASE_PASSWORD: 'your-database-password'
SECRET_KEY_BASE: 'development-secret-keybase'
test:
DATABASE_NAME: 'your-test-database'
DATABASE_USER: 'your-database-user'
DATABASE_PASSWORD: 'your-database-password'
SECRET_KEY_BASE: 'test-secret-keybase'
production:
SECRET_KEY_BASE: 'prod-secret-keybase'
After putting those keys into the application.yml, let's remove them from the secrets.yml and call them.
config/secrets.yml
development:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
test:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
One last thing, because we will write tests later let's just make our life easier by configuring all that we need now. You are definitely free to choose your own test suite, but in this tutorial I will show test examples in Minitest, instead of Rspec. I just feel that there is enough material out there for Rspec but the same can't be said for Minitest. I have used both framework in different projects, while Rspec is my first choice, for API-only app such as what we are building right now, Minitest makes a perfect choice for its lightweight and less-opinionated DSL than Rspec.
test/test_helper.rb
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'minitest/autorun'
# Capybara and shoulda for more verbose tests
require 'capybara/rails'
require 'shoulda/context'
# Improved Minitest output (color and progress bar)
require 'minitest/reporters'
require 'json'
# To generate Rspec-like test results, you know, the green dots...
Minitest::Reporters.use!(
Minitest::Reporters::ProgressReporter.new,
ENV,
Minitest.backtrace_filter
)
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
def assert_valid(model, msg = nil)
valid = model.valid?
errors = model.errors.full_messages.join(', ')
msg = message(msg) { "Expected #{model} to be valid, but got errors: #{errors}." }
assert valid, msg
end
end
class ActionDispatch::IntegrationTest
include Capybara::DSL
end
The method 'assert_valid' inside the ActiveSupport::TestCase is an addition, it makes testing validity of record easy, certainly optional but I will use it throughout this tutorial. This is my favourite setup when working with Minitest so make sure your test_helper.rb looks like that and we are good to go!
I know, I know, so far we have been doing boring stuffs. But it is better we nip those in the bud early so that we do not have to worry about them or even worse, forget to set them up entirely. But now we are ready to start having fun!
Start Having Fun
For this tutorial, we will build an API that retrieves data of provinces of a nation. In my case, since I am an Indonesian, the API will retrieves Indonesia provinces, but feel free to choose the nation for your API. To build a basic, working API basically you need: routes, controller, and model (if you have to retrieve data from database). To make our API more robust, we will also write tests that cover those three things in this post. But first, start by writing the routes.
config/routes.rb
namespace :v1 do
# Inside this will be the endpoints
end
Notice that we use namespace as a versioning method. With this, we can access all v1 endpoints via localhost:3000/v1. Now because we use namespace as the versioning method, we have to create a folder called 'v1' inside the app/controllers folder. Inside the folder, create a file that all v1 endpoint controllers will inherit from. This will make it easier for us to implement shared functionalities for all v1 endpoints. For example, if you want to force authentication on v1 endpoints, then instead of writing a before_filter in every v1 controller, we can just write it here. We are not going to implement it now, but we do want to ensure that we only accept requests with JSON format, so let's do just that:
app/controllers/v1/base_controller.rb
module V1
class BaseController < ApplicationController
# Write methods and filters that should be available to
# other endpoints on this endpoint version here
before_filter :accept_json_only_requests
def accept_json_only_requests
return if params[:format] == 'json' || request.headers['Accept'] =~ /json/
render :nothing => true, :status => 406
end
end
end
The next step is to create the table and model which will enable us to store and fetch the data of provinces. Let's use Rails generator to make the task even simpler.
bundle exec rails g model Province name abbreviation anno:date total_area:integer
The above command will generate the model file, app/models/province.rb and the migration file. Run migration to create the table.
bundle exec rails g model Province name abbreviation anno:date total_area:integer
To make it easier later, just populate the table now with some data either using a seed file or by manually inserting via rails console. We also should write model-level validation for Province since we will be accepting request to create and update provinces record. But of course, write the test first to help us stay sane:
test/models/province_test.rb
require 'test_helper'
class ProvinceTest < ActiveSupport::TestCase
def setup
@new_record = Province.new
end
test 'valid record' do
@new_record.assign_attributes(
name: 'Aceh', abbreviation: 'AC', anno: Date.parse('07-12-1959'), total_area: 57365
)
@new_record.save
assert_valid @new_record
assert_equal true, @new_record.id.present?
end
test 'presence validations' do
@new_record.valid?
attrs = [:name, :anno]
attrs.each do |att|
assert_equal true, @new_record.errors.messages.key?(att)
end
end
test 'attributes' do
assert_respond_to @new_record, :name
assert_respond_to @new_record, :abbreviation
assert_respond_to @new_record, :anno
assert_respond_to @new_record, :total_area
end
end
If you run rails test test/models/province_test.rb
you will get a failure for the validation presence assertions and that is good. Let's go green by adding this to our model:
class Province < ApplicationRecord
validates_presence_of :name, :anno
end
Now if we run the test again, we will get all green dots! Having ensure ourselves that we can store data safely, we have to write the endpoints to enable external calls to create, read, update, and destroy the data. We do not necessarily allow or have to have all those actions enabled (you may not want outside people to be able to delete your data), but for the sake of learning, we will allow it. So let's create all those endpoints for the provinces resource by inserting them in the V1 namespace, by just adding one line!.
config/routes.rb
namespace :v1 do
# Inside this will be the endpoints
resources :provinces, only: [:index, :show, :update, :create, :destroy]
end
So now we have to localhost:3000/v1/resources/ endpoints. At this moment, of course nothing will happen since we have not defined it in the controller, so let's do that. We will create the actions for retrieving a collection of provinces and a single province. But before we do that, let's write the test first. Since we will be testing fetching data from our database and serving them, might as well create some data for our test database, we can do this easily by using fixture:
test/fixtures/provinces.yml
province_one:
name: 'DKI Jakarta'
abbreviation: 'JK'
anno: <%= Date.parse('08-28-1961') %>
total_area: 661
province_two:
name: 'Bali'
abbreviation: 'BA'
anno: <%= Date.parse('08-14-1958') %>
total_area: 5561
You can setup as many as you want, but this will suffice for now.
Endpoints to Read data
We are building a habit to write test before writing every functionality. This habit in the 'wild' is called Test-Driven Development. I am not going to argue that it is the best approach in developing software, but I find it very helpful in developing APIs as when building API you have certain clear requirements and clear targets to test for, like the body of response sent by the API. Start right now by building the tests for the read endpoints:
test/controllers/v1/provinces_controller_test.rb
require 'test_helper'
class V1::ProvincesControllerTest < ActionController::TestCase
def setup
@persisted_records = Province.all
@sample = @persisted_records.first
end
# :index
test 'retrieving a collection of records' do
setup_valid_request
get :index
assert_response :ok
assert_equal Mime[:json], response.content_type
# Checking the body of the response
data_hash = JSON.parse response.body
# Check that the first item retrieved from the endpoint has the same
# id as one of the records from the test database
record_check = @persisted_records.pluck(:id).include?(data_hash[0]['id'])
assert_equal record_check, true
end
test 'get #index without correct Accept Headers' do
get :index
# Make sure that without correct accept header, the request is not acceptable
assert_equal 406, response.status
end
# :show
test 'retrieving a single record' do
setup_valid_request
get :show, params: { id: @sample.id }
assert_response :ok
assert_equal 200, response.status
record = JSON.parse response.body
assert_equal @sample.id, record['id']
attrs = ['id', 'name', 'abbreviation', 'anno', 'total_area', 'created_at', 'updated_at']
assert_equal attrs, record.keys
end
end
That is good enough starting point for the two endpoints, keep moving on we have got to make tests pass now.
app/controllers/v1/provinces_controller.rb
module V1
class ProvincesController < BaseController
def index
render status: 200,
json: Province.all.to_json
end
def show
@province = Province.find(params[:id])
render status: 200,
json: @province.to_json
end
end
end
And we passed the tests with flying colours! Also, now when we make an http request to localhost:3000/v1/provinces we will get a JSON array of provinces and a province data in JSON format if we call localhost:3000/v1/provinces/:id (the :id refers to the ID of existing stored province record). Right, now that we got the fetching data endpoints working fine, time to move to the CUD endpoints. First things first, add specs for these endpoints:
test/controllers/v1/provinces_controller_test.rb
```ruby
CREATE endpoint STARTS
test "Creating new event without correct content-type" do post :create, params: {}
assert_response 406
end
test "Creating new event with valid data" do setup_valid_request
data_hash = { name: 'Aceh', abbreviation: 'AC', anno: Date.parse('07-12-1959'), total_area: 57365 }
# Make the re