Keeping the documentation updated while the API has been developed is one of the most demanding tasks regardless of the language that you've selected for your backend.
There are many tools for managing and keeping your documentation updated like Slate, that allows you to write it in a simple way using Markdown or apipie-rails. These two provide a DSL to generate the documentation automatically. Rspec-rails-swagger also creates documentation automatically using rspec tests. In this tutorial, I'll show you how rspec-rails-swagger can help you with this task.
Preliminary Concepts
Rspec
From Wikipedia: RSpec is a 'Domain Specific Language' (DSL) testing tool written in Ruby to test Ruby code. It is a behavior-driven development (BDD) framework which is extensively used in the production applications.
Swagger
From the official site: Swagger is the world’s largest framework of API developer tools for the OpenAPI Specification(OAS), enabling development across the entire API lifecycle, from design and documentation, to test and deployment.
What is the OpenAPI Specification? At the heart of the above tools is the OpenAPI Specification (formerly called the Swagger Specification). The specification creates the RESTful contract for your API, detailing all of its resources and operations in a human and machine readable format for easy development, discovery, and integration.
Creating a new application
In this tutorial we will create a basic address book API.
rails new --api addressbook
Adding dependencies
We need to add the dependencies that we will use to create the tests. In this exercise we will use rspec-rails to develop the specs, factory_bot_rails to create database records in the tests scenarios, database_cleaner to ensure keeping a clean state during the tests, and finally rspec-rails-swagger to create the tests that will generate the documentation.
Gemfile
source 'https://rubygems.org'
git_source(:github) do |repo_name|
repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/')
"https://github.com/#{repo_name}.git"
end
gem 'rails', '~> 5.1.5'
gem 'sqlite3'
gem 'puma', '~> 3.7'
group :development, :test do
gem 'byebug', platforms: %i[mri mingw x64_mingw]
gem 'factory_bot_rails'
gem 'rspec-rails', '~> 3.7.0'
gem 'rspec-rails-swagger', '~> 0.1.4'
end
group :development do
gem 'listen', '>= 3.0.5', ' 2.0.0'
end
group :test do
gem 'database_cleaner'
end
gem 'tzinfo-data', platforms: %I[mingw mswin x64_mingw jruby]
Don’t forget to install the new dependencies.
bundle install
Rspec
To install rspec we need to run the generator that adds the basic configuration.
rails generate rspec:install
You can test your config with the following command:
bundle exec rspec
Factory bot
To complete the Factory bot setup, add the following code at the beginning of the document:
spec/rails_helper.rb
require 'factory_bot_rails'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
You should have something like this:
spec/rails_helper.rb
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
require 'factory_bot_rails'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
And add the following file:
spec/support/factory_bot.rb
# spec/support/factory_bot.rb
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
Database Cleaner
To get the database cleaner working you need to add the following configuration inside the RSpec.configure block:
spec/rails_helper.rb
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
Rspec Rails Swagger
Add the following configuration inside the RSpec.configure block:
spec/rails_helper.rb
config.backtrace_inclusion_patterns = [%r{app|spec}]
Add the following file, we will use it to add the swagger definitions:
spec/swagger_helper.rb
require 'rspec/rails/swagger'
require 'rails_helper'
RSpec.configure do |config|
# Specify a root directory where the generated Swagger files will be saved.
config.swagger_root = Rails.root.to_s + '/swagger'
# Define one or more Swagger documents and global metadata for each.
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
},
definitions: {
}
}
}
end
Creating the contact migration
We will add a migration for the contacts table, where we will store name, phone, email and address.
rails g migration create_contacts
XXX_create_contacts.rb
class CreateContacts < ActiveRecord::Migration[5.1]
def change
create_table :contacts do |t|
t.string :name, null: false
t.string :phone, null: false
t.string :email
t.string :address
t.timestamps
end
end
end
Run the migrations.
rails db:migrate
Adding Contact Model and factory
Before start developing the endpoints we need to add the Contact model and the factory that we will use on the rspec tests.
app/models/contact.rb
class Contact < ApplicationRecord
validates :name, :phone, presence: true
end
spec/factories/contacts.rb
FactoryBot.define do
factory :contact do
sequence :name do |n|
"John Doe #{n}"
end
phone { rand(10**9..10**10) }
sequence :email do |n|
"john-doe#{n}@test.magmalabs.io"
end
sequence :address do |n|
"Fake Address #{n}"
end
end
end
Creating the contact index endpoint
For this first endpoint, we need to add the contact resource on the routes file and specify the index endpoint.
config/routes.rb
Rails.application.routes.draw do
resources :contacts, as: :users, only: [:index]
end
We can start writing the index test. We need to define the contacts path and inside it, we will define the GET operation that calls the index method. In this case, we test the response code is 200 and the body returns 3 contact records.
spec/requests/contacts_request_spec.rb
require 'swagger_helper'
RSpec.describe 'Contacts', type: :request, capture_examples: true do
path '/contacts' do
get(summary: 'Get contacts') do
consumes 'application/json'
produces 'application/json'
tags :contacts
let!(:contacts) do
3.times do
create(:contact)
end
end
response(200, description: 'Return all the available contacts') do
it 'Return 3 contacts' do
body = JSON(response.body)
expect(body.count).to eq(3)
end
end
end
end
end
On the contacts controller we need to define the index method that is called when the request is performed.
app/controllers/contacts_controller.rb
class ContactsController < ApplicationController
def index
response = Contact.all
render json: response, status: :ok
end
end
Creating the contact create endpoint
To create the endpoint we need to specify the method on the contact resource.
config/routes.rb
Rails.application.routes.draw do
resources :contacts, as: :users, only: %I[index create]
end
To create and to update endpoints, we will need to specify in the swagger helper file the data models based on the swagger specification. You can learn more about it on the project page.
spec/swagger_helper.rb
require 'rspec/rails/swagger'
require 'rails_helper'
RSpec.configure do |config|
# Specify a root directory where the generated Swagger files will be saved.
config.swagger_root = Rails.root.to_s + '/swagger'
# Define one or more Swagger documents and global metadata for each.
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
},
definitions: {
createContact: {
type: :object,
properties: {
data: {
type: :object,
required: %i[name phone email address],
properties: {
name: { type: :string, example: 'magmaLabs.io' },
phone: { type: :string, example: '+52 1 667 317 9035' },
email: { type: :string, example: 'hello@magmalabs.io' },
address: { type: :string, example: 'Av. Constitución #2035. Colima, Colima, MX, 28017' }
}
}
}
}
}
}
}
end
Once we have defined the model, we can proceed to set the POST method in the contacts test path. Unlike the index test, we need to add the parameter method to document the body of our request. In this case, we are only testing if the response returns 201. You can improve the test as much as you want.
spec/requests/contacts_request_spec.rb
require 'swagger_helper'
RSpec.describe 'Contacts', type: :request, capture_examples: true do
path '/contacts' do
get(summary: 'Get contacts') do
...
end
post(summary: 'Create a new contact') do
consumes 'application/json'
produces 'application/json'
tags :contacts
parameter :data,
in: :body,
required: true,
schema: {
'$ref' => '#/definitions/createContact'
}
response(201, description: 'Contact created') do
let(:data) do
{
data: {
name: 'Magmalabs.io',
phone: '+52 1 667 317 9035',
email: 'hello@magmalabs.io',
address: 'Av. Constitución #2035. Colima, Colima, MX, 28017'
}
}
end
end
end
end
end
Again we will have to define the logic for the method we are testing.
app/controllers/contacts_controller.rb
class ContactsController < ApplicationController
...
def create
response = Contact.create(create_contact_params)
render json: response, status: :created
end
private
def create_contact_params
params.require(:data).permit(:name, :phone, :email, :address)
end
end
Creating the contact show endpoint
As in previous cases we need to add the show method in the contact resource.
config/routes.rb
Rails.application.routes.draw do
resources :contacts, as: :users, only: %I[index create show]
end
The test for the show method is very similar to the index with the only difference that you have to input the id of the contact you want to get.
spec/requests/contacts_request_spec.rb
require 'swagger_helper'
RSpec.describe 'Contacts', type: :request, capture_examples: true do
path '/contacts' do
...
end
path '/contacts/{id}' do
get(summary: 'Get Contact') do
consumes 'application/json'
produces 'application/json'
tags :contacts
parameter :id, in: :path, type: :integer, required: true, description: 'Contact ID'
let(:contact_1) do
create(:contact)
end
let(:contact_2) do
create(:contact)
end
response(200, description: 'Return the selected contact') do
let(:id) { contact_1.id }
end
response(404, description: 'Contact not found') do
let(:id) { 999 }
end
end
end
end
As in the last two examples, we need to add the required logic for the show endpoint.
app/controllers/contacts_controller.rb
class ContactsController < ApplicationController
before_action :find_contact, only: [:show]
...
def show
render json: @contact, status: :ok
end
private
…
def find_contact
@contact = Contact.where(id: params[:id]).first
render :nothing, status: :not_found unless @contact
end
end
Creating the contact update endpoint
config/routes.rb
Rails.application.routes.draw do
resources :contacts, as: :users, only: %I[index create show update]
end
For this example, we will allow to update all the contact data, so we will use the same definition that we had for the create method, so we should change the definition name from createContact to contact.
spec/swagger_helper.rb
require 'rspec/rails/swagger'
require 'rails_helper'
RSpec.configure do |config|
# Specify a root directory where the generated Swagger files will be saved.
config.swagger_root = Rails.root.to_s + '/swagger'
# Define one or more Swagger documents and global metadata for each.
config.swagger_docs = {
'v1/swagger.json' => {
swagger: '2.0',
info: {
title: 'API V1',
version: 'v1'
},
definitions: {
contact: {
type: :object,
properties: {
data: {
type: :object,
required: %i[name phone email address],
properties: {
name: { type: :string, example: 'magmaLabs.io' },
phone: { type: :string, example: '+52 1 667 317 9035' },
email: { type: :string, example: 'hello@magmalabs.io' },
address: { type: :string, example: 'Av. Constitución #2035. Colima, Colima, MX, 28017' }
}
}
}
}
}
}
}
end
The update test is inside the contact/{id} path because we need to specify which contact is being updated. This test is similar to the create test.
spec/requests/contacts_request_spec.rb
spec/requests/contacts_request_spec.rb
require 'swagger_helper'
RSpec.describe 'Contacts', type: :request, capture_examples: true do
path '/contacts' do
...
end
path '/contacts/{id}' do
...
put(summary: 'Update Contact') do
consumes 'application/json'
produces 'application/json'
tags :contacts
parameter :id, in: :path, type: :integer, required: true, description: 'Contact ID'
parameter :data,
in: :body,
required: true,
schema: {
'$ref' => '#/definitions/contact'
}
let(:contact_1) do
create(:contact)
end
response(200, description: 'Contact updated') do
let(:id) { contact_1.id }
let(:data) do
{
data: {
name: 'Magmalabs.io',
phone: '+52 1 667 317 9035',
email: 'hello@magmalabs.io',
address: 'Av. Constitución #2035. Colima, Colima, MX, 28017'
}
}
end
end
response(404, description: 'Contact not found') do
let(:id) { 999 }
let(:data) do
{
data: {
name: 'Magmalabs.io',
phone: '+52 1 667 317 9035',
email: 'hello@magmalabs.io',
address: 'Av. Constitución #2035. Colima, Colima, MX, 28017'
}
}
end
end
end
end
end
As in previous cases, we should implement the required logic to cover the tests.
app/controllers/contacts_controller.rb
class ContactsController < ApplicationController
before_action :find_contact, only: %I[show update]
def index
...
end
def create
response = Contact.create(contact_params)
render json: response, status: :created
end
...
def update
response = @contact.update(contact_params)
render :nothing, status: :ok
end
private
def contact_params
params.require(:data).permit(:name, :phone, :email, :address)
end
...
end
Generating the swagger docs
Finally, you can run your specs with the rspec command, but if you want to generate the swagger doc file, you ought to use rails swagger to generate a swagger.json file inside the swagger folder.
You can visualize this file in any client that allows swagger files. I used to use the Official Swagger Editor that allows you to load the file and check the structure in a more comfortable way.
The best way to keep your repetitive process up to date is to automate them. Using rspec rails swagger to document your APIs can save you a lot of time. Your documentation will always be updated and supported by tests that ensure your endpoints will work. If you want to check the project source code, you can visit the following repository.