Build a RESTful JSON API With Rails 5 – Part One

Rails is popularly known for building web applications. Chances are if you’re reading this you’ve built a traditional server-rendered web application with Rails before. If not, I’d highly recommend going through the Getting Started with Rails page to familiarize yourself with the Rails framework before proceeding with this tutorial.

As of version 5, Rails core now supports API only applications! In previous versions, we relied on an external gem: rails-api which has since been merged to core rails.

API only applications are slimmed down compared to traditional Rails web applications. According to Rails 5 release notes, generating an API only application will:

  • Start the application with a limited set of middleware
  • Make the ApplicationController inherit from ActionController::API instead of ActionController::Base
  • Skip generation of view files

This works to generate an API-centric framework excluding functionality that would otherwise be unused and unnecessary.

In this three-part tutorial, we’ll build a todo list API where users can manage their to-do lists and todo items.

Prerequisites

Before we begin, make sure you have ruby version >=2.2.2 and rails version 5.

$ ruby -v # ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin16] $ rails -v # Rails 5.0.1 

If your ruby version is not up to date, you can update it with a ruby version manager like rvm or rbenv.

# when using rbenv $ rbenv install 2.3.1 # set 2.3.1 as the global version $ rbenv global 2.3.1 
# when using rvm $ rvm install 2.3.1 # set 2.3.1 as the global version $ rvm use 2.3.1 

If your rails version is not up to date, update to the latest version by running:

$ gem update rails 

All good? Let’s get started!

API Endpoints

Our API will expose the following RESTful endpoints.

Endpoint Functionality
POST /signup Signup
POST /auth/login Login
GET /auth/logout Logout
GET /todos List all todos
POST /todos Create a new todo
GET /todos/:id Get a todo
PUT /todos/:id Update a todo
DELETE /todos/:id Delete a todo and its items
GET /todos/:id/items Get a todo item
PUT /todos/:id/items Update a todo item
DELETE /todos/:id/items Delete a todo item

Part One will Cover:

  • Project setup
  • Todos API
  • TodoItems API

Project Setup

Generate a new project todos-api by running:

$ rails new todos-api --api -T 

Note that we’re using the --api argument to tell Rails that we want an API application and -T to exclude Minitest the default
testing framework. Don’t freak out, we’re going to write tests. We’ll be using RSpec instead to test our API. I find RSpec to be more expressive
and easier to start with as compared to Minitest.

Dependencies

Let’s take a moment to review the gems that we’ll be using.

  • rspec-rails – Testing framework.
  • factorybotrails – A fixtures replacement with a more straightforward syntax. You’ll see.
  • shoulda_matchers – Provides RSpec with additional matchers.
  • database_cleaner – You guessed it! It literally cleans our test database to ensure a clean state in each test suite.
  • faker – A library for generating fake data. We’ll use this to generate test data.

All good? Great! Let’s set them up. In your Gemfile:

Add rspec-rails to both the :development and :test groups.

# Gemfile group :development, :test do   gem 'rspec-rails', '~> 3.5' end 

This is a handy shorthand to include a gem in multiple environments.

Add factory_bot_rails, shoulda_matchers, faker and database_cleaner to the :test group.

# Gemfile group :test do   gem 'factory_bot_rails', '~> 4.0'   gem 'shoulda-matchers', '~> 3.1'   gem 'faker'   gem 'database_cleaner' end 

Install the gems by running:

$ bundle install 

Initialize the spec directory (where our tests will reside).

$ rails generate rspec:install 

This adds the following files which are used for configuration:

  • .rspec
  • spec/spec_helper.rb
  • spec/rails_helper.rb

Create a factories directory (factory bot uses this as the default directory). This is where we’ll define the model factories.

$ mkdir spec/factories 

Configuration

In spec/rails_helper.rb

# require database cleaner at the top level require 'database_cleaner'  # [...] # configure shoulda matchers to use rspec as the test framework and full matcher libraries for rails Shoulda::Matchers.configure do |config|   config.integrate do |with|     with.test_framework :rspec     with.library :rails   end end  # [...] RSpec.configure do |config|   # [...]   # add `FactoryBot` methods   config.include FactoryBot::Syntax::Methods    # start by truncating all the tables but then use the faster transaction strategy the rest of the time.   config.before(:suite) do     DatabaseCleaner.clean_with(:truncation)     DatabaseCleaner.strategy = :transaction   end    # start the transaction strategy as examples are run   config.around(:each) do |example|     DatabaseCleaner.cleaning do       example.run     end   end   # [...] end 

Phew! That was a rather long. Good thing is, it’s a smooth ride from here on out.

Models

Let’s start by generating the Todo model

$ rails g model Todo title:string created_by:string 

Notice that we’ve included the model attributes in the model generation command. This way we don’t have to edit the migration file.
The generator invokes active record and rspec to generate the migration, model, and spec respectively.

# db/migrate/[timestamp]_create_todos.rb class CreateTodos < ActiveRecord::Migration[5.0]   def change     create_table :todos do |t|       t.string :title       t.string :created_by        t.timestamps     end   end end 

And now the Item model

$ rails g model Item name:string done:boolean todo:references 

By adding todo:references we’re telling the generator to set up an association with the Todo model.
This will do the following:

  • Add a foreign key column todo_id to the items table
  • Setup a belongs_to association in the Item model
# db/migrate/[timestamp]_create_items.rb class CreateItems < ActiveRecord::Migration[5.0]   def change     create_table :items do |t|       t.string :name       t.boolean :done       t.references :todo, foreign_key: true        t.timestamps     end   end end 

Looks good? Let’s run the migrations.

$ rails db:migrate 

We’re Test Driven, let’s write the model specs first.

# spec/models/todo_spec.rb require 'rails_helper'  # Test suite for the Todo model RSpec.describe Todo, type: :model do   # Association test   # ensure Todo model has a 1:m relationship with the Item model   it { should have_many(:items).dependent(:destroy) }   # Validation tests   # ensure columns title and created_by are present before saving   it { should validate_presence_of(:title) }   it { should validate_presence_of(:created_by) } end 

RSpec has a very expressive DSL (Domain Specific Language). You can almost read the tests like a paragraph.
Remember our shoulda matchers gem? It provides RSpec with the nifty association and validation matchers above.

# spec/models/item_spec.rb require 'rails_helper'  # Test suite for the Item model RSpec.describe Item, type: :model do   # Association test   # ensure an item record belongs to a single todo record   it { should belong_to(:todo) }   # Validation test   # ensure column name is present before saving   it { should validate_presence_of(:name) } end 

Let’s execute the specs by running:

$ bundle exec rspec 

And to no surprise, we have only one test passing and four failures. Let’s go ahead and fix the failures.

# app/models/todo.rb class Todo < ApplicationRecord   # model association   has_many :items, dependent: :destroy    # validations   validates_presence_of :title, :created_by end 
# app/models/item.rb class Item < ApplicationRecord   # model association   belongs_to :todo    # validation   validates_presence_of :name end 

At this point run the tests again and…

voila! All green.

Controllers

Now that our models are all setup, let’s generate the controllers.

$ rails g controller Todos $ rails g controller Items 

You guessed it! Tests first… with a slight twist. Generating controllers by default generates controller specs.
However, we won’t be writing any controller specs. We’re going to write request specs instead.

Request specs are designed to drive behavior through the full stack, including routing. This means they can hit the applications’
HTTP endpoints as opposed to controller specs which call methods directly. Since we’re building an API application, this is exactly the kind of behavior we want from our tests.

According to RSpec, the official recommendation of the Rails team and the RSpec core team is to write request specs instead.

Add a requests folder to the spec directory with the corresponding spec files.

$ mkdir spec/requests && touch spec/requests/{todos_spec.rb,items_spec.rb}  

Before we define the request specs, Let’s add the model factories which will provide the test data.

Add the factory files:

$ touch spec/factories/{todos.rb,items.rb} 

Define the factories.

# spec/factories/todos.rb FactoryBot.define do   factory :todo do     title { Faker::Lorem.word }     created_by { Faker::Number.number(10) }   end end 

By wrapping faker methods in a block, we ensure that faker generates dynamic data every time the factory is invoked.
This way, we always have unique data.

# spec/factories/items.rb FactoryBot.define do   factory :item do     name { Faker::StarWars.character }     done false     todo_id nil   end end 

Todo API

# spec/requests/todos_spec.rb require 'rails_helper'  RSpec.describe 'Todos API', type: :request do   # initialize test data    let!(:todos) { create_list(:todo, 10) }   let(:todo_id) { todos.first.id }    # Test suite for GET /todos   describe 'GET /todos' do     # make HTTP get request before each example     before { get '/todos' }      it 'returns todos' do       # Note `json` is a custom helper to parse JSON responses       expect(json).not_to be_empty       expect(json.size).to eq(10)     end      it 'returns status code 200' do       expect(response).to have_http_status(200)     end   end    # Test suite for GET /todos/:id   describe 'GET /todos/:id' do     before { get "/todos/#{todo_id}" }      context 'when the record exists' do       it 'returns the todo' do         expect(json).not_to be_empty         expect(json['id']).to eq(todo_id)       end        it 'returns status code 200' do         expect(response).to have_http_status(200)       end     end      context 'when the record does not exist' do       let(:todo_id) { 100 }        it 'returns status code 404' do         expect(response).to have_http_status(404)       end        it 'returns a not found message' do         expect(response.body).to match(/Couldn't find Todo/)       end     end   end    # Test suite for POST /todos   describe 'POST /todos' do     # valid payload     let(:valid_attributes) { { title: 'Learn Elm', created_by: '1' } }      context 'when the request is valid' do       before { post '/todos', params: valid_attributes }        it 'creates a todo' do         expect(json['title']).to eq('Learn Elm')       end        it 'returns status code 201' do         expect(response).to have_http_status(201)       end     end      context 'when the request is invalid' do       before { post '/todos', params: { title: 'Foobar' } }        it 'returns status code 422' do         expect(response).to have_http_status(422)       end        it 'returns a validation failure message' do         expect(response.body)           .to match(/Validation failed: Created by can't be blank/)       end     end   end    # Test suite for PUT /todos/:id   describe 'PUT /todos/:id' do     let(:valid_attributes) { { title: 'Shopping' } }      context 'when the record exists' do       before { put "/todos/#{todo_id}", params: valid_attributes }        it 'updates the record' do         expect(response.body).to be_empty       end        it 'returns status code 204' do         expect(response).to have_http_status(204)       end     end   end    # Test suite for DELETE /todos/:id   describe 'DELETE /todos/:id' do     before { delete "/todos/#{todo_id}" }      it 'returns status code 204' do       expect(response).to have_http_status(204)     end   end end 

We start by populating the database with a list of 10 todo records (thanks to factory bot).
We also have a custom helper method json which parses the JSON response to a Ruby Hash which is easier to work with in our tests.
Let’s define it in spec/support/request_spec_helper.

Add the directory and file:

$ mkdir spec/support && touch spec/support/request_spec_helper.rb 
# spec/support/request_spec_helper module RequestSpecHelper   # Parse JSON response to ruby hash   def json     JSON.parse(response.body)   end end 

The support directory is not autoloaded by default. To enable this, open the rails helper and comment out the support directory auto-loading and then
include it as shared module for all request specs in the RSpec configuration block.

# spec/rails_helper.rb # [...] Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } # [...] RSpec.configuration do |config|   # [...]   config.include RequestSpecHelper, type: :request   # [...] end 

Run the tests.

We get failing routing errors. This is because we haven’t defined the routes yet. Go ahead and define them in config/routes.rb.

# config/routes.rb Rails.application.routes.draw do   resources :todos do     resources :items   end end 

In our route definition, we’re creating todo resource with a nested items resource. This enforces the 1:m (one to many) associations at the routing level.
To view the routes, you can run:

$ rails routes 

When we run the tests we see that the routing error is gone. As expected we have controller failures. Let’s go ahead and define the controller methods.

# app/controllers/todos_controller.rb class TodosController < ApplicationController   before_action :set_todo, only: [:show, :update, :destroy]    # GET /todos   def index     @todos = Todo.all     json_response(@todos)   end    # POST /todos   def create     @todo = Todo.create!(todo_params)     json_response(@todo, :created)   end    # GET /todos/:id   def show     json_response(@todo)   end    # PUT /todos/:id   def update     @todo.update(todo_params)     head :no_content   end    # DELETE /todos/:id   def destroy     @todo.destroy     head :no_content   end    private    def todo_params     # whitelist params     params.permit(:title, :created_by)   end    def set_todo     @todo = Todo.find(params[:id])   end end 

More helpers. Yay! This time we have:

  • json_response which does… yes, responds with JSON and an HTTP status code (200 by default). We can define this method in concerns folder.
# app/controllers/concerns/response.rb module Response   def json_response(object, status = :ok)     render json: object, status: status   end end 
  • set_todo – callback method to find a todo by id. In the case where the record does not exist, ActiveRecord will throw an exception ActiveRecord::RecordNotFound. We’ll rescue from this exception and return a 404 message.
# app/controllers/concerns/exception_handler.rb module ExceptionHandler   # provides the more graceful `included` method   extend ActiveSupport::Concern    included do     rescue_from ActiveRecord::RecordNotFound do |e|       json_response({ message: e.message }, :not_found)     end      rescue_from ActiveRecord::RecordInvalid do |e|       json_response({ message: e.message }, :unprocessable_entity)     end   end end 

In our create method in the TodosController, note that we’re using create! instead of create. This way, the model will raise
an exception ActiveRecord::RecordInvalid. This way, we can avoid deep nested if statements in the controller. Thus, we rescue from this exception
in the ExceptionHandler module.

However, our controller classes don’t know about these helpers yet. Let’s fix that by including these modules in the
application controller.

# app/controllers/application_controller.rb class ApplicationController < ActionController::API   include Response   include ExceptionHandler end 

Run the tests and everything’s all green!

Let’s fire up the server for some good old manual testing.

$ rails s 

Now let’s go ahead and make requests to the API. I’ll be using httpie as my HTTP client.

# GET /todos $ http :3000/todos # POST /todos $ http POST :3000/todos title=Mozart created_by=1 # PUT /todos/:id $ http PUT :3000/todos/1 title=Beethoven # DELETE /todos/:id $ http DELETE :3000/todos/1 

You should see similar output.

TodoItems API

# spec/requests/items_spec.rb require 'rails_helper'  RSpec.describe 'Items API' do   # Initialize the test data   let!(:todo) { create(:todo) }   let!(:items) { create_list(:item, 20, todo_id: todo.id) }   let(:todo_id) { todo.id }   let(:id) { items.first.id }    # Test suite for GET /todos/:todo_id/items   describe 'GET /todos/:todo_id/items' do     before { get "/todos/#{todo_id}/items" }      context 'when todo exists' do       it 'returns status code 200' do         expect(response).to have_http_status(200)       end        it 'returns all todo items' do         expect(json.size).to eq(20)       end     end      context 'when todo does not exist' do       let(:todo_id) { 0 }        it 'returns status code 404' do         expect(response).to have_http_status(404)       end        it 'returns a not found message' do         expect(response.body).to match(/Couldn't find Todo/)       end     end   end    # Test suite for GET /todos/:todo_id/items/:id   describe 'GET /todos/:todo_id/items/:id' do     before { get "/todos/#{todo_id}/items/#{id}" }      context 'when todo item exists' do       it 'returns status code 200' do         expect(response).to have_http_status(200)       end        it 'returns the item' do         expect(json['id']).to eq(id)       end     end      context 'when todo item does not exist' do       let(:id) { 0 }        it 'returns status code 404' do         expect(response).to have_http_status(404)       end        it 'returns a not found message' do         expect(response.body).to match(/Couldn't find Item/)       end     end   end    # Test suite for PUT /todos/:todo_id/items   describe 'POST /todos/:todo_id/items' do     let(:valid_attributes) { { name: 'Visit Narnia', done: false } }      context 'when request attributes are valid' do       before { post "/todos/#{todo_id}/items", params: valid_attributes }        it 'returns status code 201' do         expect(response).to have_http_status(201)       end     end      context 'when an invalid request' do       before { post "/todos/#{todo_id}/items", params: {} }        it 'returns status code 422' do         expect(response).to have_http_status(422)       end        it 'returns a failure message' do         expect(response.body).to match(/Validation failed: Name can't be blank/)       end     end   end    # Test suite for PUT /todos/:todo_id/items/:id   describe 'PUT /todos/:todo_id/items/:id' do     let(:valid_attributes) { { name: 'Mozart' } }      before { put "/todos/#{todo_id}/items/#{id}", params: valid_attributes }      context 'when item exists' do       it 'returns status code 204' do         expect(response).to have_http_status(204)       end        it 'updates the item' do         updated_item = Item.find(id)         expect(updated_item.name).to match(/Mozart/)       end     end      context 'when the item does not exist' do       let(:id) { 0 }        it 'returns status code 404' do         expect(response).to have_http_status(404)       end        it 'returns a not found message' do         expect(response.body).to match(/Couldn't find Item/)       end     end   end    # Test suite for DELETE /todos/:id   describe 'DELETE /todos/:id' do     before { delete "/todos/#{todo_id}/items/#{id}" }      it 'returns status code 204' do       expect(response).to have_http_status(204)     end   end end 

As expected, running the tests at this point should output failing todo item tests. Let’s define the todo items controller.

# app/controllers/items_controller.rb class ItemsController < ApplicationController   before_action :set_todo   before_action :set_todo_item, only: [:show, :update, :destroy]    # GET /todos/:todo_id/items   def index     json_response(@todo.items)   end    # GET /todos/:todo_id/items/:id   def show     json_response(@item)   end    # POST /todos/:todo_id/items   def create     @todo.items.create!(item_params)     json_response(@todo, :created)   end    # PUT /todos/:todo_id/items/:id   def update     @item.update(item_params)     head :no_content   end    # DELETE /todos/:todo_id/items/:id   def destroy     @item.destroy     head :no_content   end    private    def item_params     params.permit(:name, :done)   end    def set_todo     @todo = Todo.find(params[:todo_id])   end    def set_todo_item     @item = @todo.items.find_by!(id: params[:id]) if @todo   end end 

Run the tests.

Run some manual tests for the todo items API:

# GET /todos/:todo_id/items $ http :3000/todos/2/items # POST /todos/:todo_id/items $ http POST :3000/todos/2/items name='Listen to 5th Symphony' done=false # PUT /todos/:todo_id/items/:id $ http PUT :3000/todos/2/items/1 done=true # DELETE /todos/:todo_id/items/1 $ http DELETE :3000/todos/2/items/1 

Conclusion

That’s it for part one! At this point you should have learned how to:

  • Generate an API application with Rails 5
  • Setup RSpec testing framework with Factory Bot, Database Cleaner, Shoulda Matchers and Faker.
  • Build models and controllers with TDD (Test Driven Development).
  • Make HTTP requests to an API with httpie.

In the next part, we’ll cover authentication with JWT, pagination, and API versioning. Hope to see you there. Cheers!