Generating OpenAPI docs for Ruby on Rails with RSwag
API Code-first is the art of building an API, and then popping some annotations or metadata in there to output API documentation in an API description format like OpenAPI.
The most popular API Code-first approach in Ruby on Rails uses a tool called RSwag. With RSwag you write the OpenAPI for each API endpoint into special tests, which help to confirm your responses are matching the OpenAPI you’ve just written.
Creating OpenAPI with RSwag #
This guide assumes you want to use RSpec
as your testing framework in your application.
Step 1: Add the rswag dependencies to the Gemfile
.
# Gemfile
gem 'rspec-rails'
gem 'rswag'
Step 2: Install the new dependencies and run the rswag generator.
bundle install
You’ll need to run the rspec generator if it’s the first time you use rspec.
rails generate rspec:install
Then run the rswag generator.
rails generate rswag:install
Step 3: You’ll need some controllers/models to describe, which your application probably already has. If you need to make some, use the following scaffold command to add everything Rails related.
rails generate scaffold Widget name:string
rails db:migrate
Step 4: With controllers and models in the app, we can use the rspec:swagger
generator referencing a valid controller class name to make an OpenAPI test. _If you’re wondering why its called “Swagger”, that’s the old name for OpenAPI, and with some tools it’s just stuck.
rails generate rspec:swagger WidgetsController
Step 5: This will create spec/requests/widgets_spec.rb
which will look like this:
require 'swagger_helper'
RSpec.describe 'widgets', type: :request do
path '/widgets' do
get('list widgets') do
response(200, 'successful') do
after do |example|
example.metadata[:response][:content] = {
'application/json' => {
example: JSON.parse(response.body, symbolize_names: true)
}
}
end
run_test!
end
end
post('create widget') do
response(200, 'successful') do
after do |example|
example.metadata[:response][:content] = {
'application/json' => {
example: JSON.parse(response.body, symbolize_names: true)
}
}
end
run_test!
end
end
end
path '/widgets/{id}' do
# You'll want to customize the parameter types...
parameter name: 'id', in: :path, type: :string, description: 'id'
get('show widget') do
response(200, 'successful') do
let(:id) { '123' }
after do |example|
example.metadata[:response][:content] = {
'application/json' => {
example: JSON.parse(response.body, symbolize_names: true)
}
}
end
run_test!
end
end
put('update widget') do
response(200, 'successful') do
let(:id) { '123' }
after do |example|
example.metadata[:response][:content] = {
'application/json' => {
example: JSON.parse(response.body, symbolize_names: true)
}
}
end
run_test!
end
end
# snipped patch and delete for readability
end
Step 6: All of this test is not just checking the code is doing what we expect, but it’s actually able to produce OpenAPI for us. Let’s see what we’ve got so far.
$ rake rswag
Generating Swagger docs ...
Swagger doc generated at /Users/phil/src/rails-code-first/swagger/v1/swagger.yaml
Step 7: OpenAPI being generated means we can deploy it to Bump.sh.
Deploying OpenAPI documentation to Bump.sh.
- Create and name your first API documentation.
- Then, retrieve the name and token of this documentation from the CI deployment settings page.
Step 8: Install the Bump.sh CLI with npm.
npm install -g bump-cli
Step 9: The moment you’ve been waiting for, deploying your OpenAPI to Bump.sh to generated beautiful hosted documentation!
$ bump deploy swagger/v1/swagger.yaml \
--doc my-documentation-name \
--token my-documentation-token
* Your new documentation version will soon be ready at https://bump.sh/bump-examples/hub/code-samples/doc/rails-hello-openapi
Step 10: Head over to your documentation and see how it looks at this early stage.
It looks like a start, but it’s missing a whole lot of the what, and the why, which is crucial to making API documentation useful, relevant, and readable. Time to learn a bit more about the RSwag DSL and get more data into our docs.
Step 11: Going back to spec/requests/widgets_spec.rb
we leverage the DSL to improve our tests, and improve our OpenAPI. Lets add some headers, a schema to explain how the object is going to look, and a few error responses.
path '/widgets' do
get 'list widgets' do
produces 'application/json'
response 200, 'successful' do
header 'Cache-Control', schema: { type: :string }, description: <<~HEADER
This header declares the cacheability of the content so you can skip repeating requests.
Values can be `max-age`, `must-revalidate` and `private`. It can also combine any of those separated by a comma. E.g. `Cache-Control: max-age=604800, must-revalidate`
HEADER
schema type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'uuid',
example: '123e4567-e89b-12d3-a456-426614174000',
},
title: {
type: 'string',
example: 'Neuralyzer',
},
created_at: {
type: 'string',
format: 'date-time',
},
updated_at: {
type: 'string',
format: 'date-time',
},
}
}
example 'application/json', :example_key, [
{
id: 1,
title: 'Neuralyzer',
}
]
run_test!
end
response 429, 'too many requests' do
header 'X-Rate-Limit-Limit', schema: { type: :integer }, description: 'The number of allowed requests in the current period'
header 'X-Rate-Limit-Remaining', schema: { type: :integer }, description: 'The number of remaining requests in the current period'
header 'X-Rate-Limit-Reset', schema: { type: :integer }, description: 'The number of seconds left in the current period'
run_test!
end
end
This is a lot, but let’s walk through some of those changes.
The produces: application/json
explains that the output is JSON, and seems to fix a bug in rswag where a lot of other OpenAPI won’t show up.
Then in the 200 response we’ve described headers, including this one for cache controls so clients know they can use client caching middleware if they want to cut down on wasteful repeat requests. Please note we have used a multiline text to describe the header in the OpenAPI document and that it supports Markdown.
response 200, 'successful' do
header 'Cache-Control', schema: { type: :string }, description: <<~HEADER
This header declares the cacheability of the content so you can skip repeating requests.
Values can be `max-age`, `must-revalidate` and `private`. It can also combine any of those separated by a comma. E.g. `Cache-Control: max-age=604800, must-revalidate`
HEADER
The schema block is OpenAPI, but written in Ruby syntax instead of JSON or YAML so we can cut down on some of the parenthesis. This explains what properties can be expected in the response body, and what format they’ll use (UUID, date time instead of unix, etc.) You can even provide an example for the property, to help Bump automatically construct an example for the whole response body to avoid needing to do that yourself.
schema type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'uuid',
example: '123e4567-e89b-12d3-a456-426614174000',
},
title: {
type: 'string',
example: 'Neuralyzer',
},
created_at: {
type: 'string',
format: 'date-time',
},
updated_at: {
type: 'string',
format: 'date-time',
},
}
}
Try making similar changes yourself and running rake rswag
again to update swagger/v1/swagger.yaml
. Then either use bump deploy
to update your hosted documentation, or bump preview --live --open swagger/v1/swagger.yaml
to see how it looks without deploying it everytime your make a change in your spec file.
The RSwag DSL documentation can help you with all sorts of improvements, including creating examples, adding security schemes, and using $ref
to reduce repetition in your OpenAPI.
Sample Code #
The sample code for this guide is published on GitHub so you can try that if you’re having trouble adding it to your application: rails-code-first, and the deployed documentation is over here.
Honorable Mentions #
If rswag is not working out for you, take a look at some of these tools.