Adding Interactive API Documentation to Rails with Swagger and rswag
We recently added API documentation to our Rails application. The app has multiple API namespaces serving different use cases: a partner API, public endpoints, and internal endpoints.
The goal was straightforward: create documentation that our team could reference, but more importantly, that we could share with customers who integrate with our APIs. The twist? We often need to share individual endpoint documentation via email. A customer asks about one specific endpoint, and we want to send them exactly what they need—not the whole documentation. Swagger lets you copy a curl example for any endpoint, which is perfect for this workflow
This requirement led us to choose Swagger UI over ReDoc.
Why Swagger UI Over ReDoc
Both Swagger UI and ReDoc render OpenAPI specifications beautifully. ReDoc arguably has a cleaner, more modern interface. But it’s read-only.
Swagger UI has the “Try it out” feature. You click a button, fill in parameters, hit Execute, and see the actual API response. Customers can test authentication, verify response formats, and catch issues before writing any code. When I need to share endpoint details with external stakeholders, I can send them a curl example via email.
Setting Up rswag
The rswag gem is a wrapper around Swagger UI that integrates with RSpec. You write your API specs as RSpec tests, and rswag generates OpenAPI YAML files from those specs. The approach feels natural in a Rails app already using RSpec.
We added three gems to our Gemfile:
# API Documentation
gem "rswag-api"
gem "rswag-ui"
group :test, :development do
gem "rswag-specs"
end
After running bundle install, we set up the configuration files.
Configuring the Swagger endpoint
In config/initializers/rswag_api.rb, we pointed rswag at the directory where our OpenAPI YAML files live:
Rswag::Api.configure do |c|
c.openapi_root = Rails.root.join('swagger').to_s
end
In config/initializers/rswag_ui.rb, we configured the UI and added a critical security control:
Rswag::Ui.configure do |c|
c.openapi_endpoint '/admin/api-docs/v1/swagger.yaml', 'API V1'
c.config_object[:supportedSubmitMethods] = []
end
Defining the OpenAPI Spec
The spec/swagger_helper.rb file defines the global structure of our API documentation. This includes metadata, authentication schemes, reusable schemas, and tags.
RSpec.configure do |config|
config.openapi_root = Rails.root.join('swagger').to_s
config.openapi_specs = {
'v1/swagger.yaml' => {
openapi: '3.0.1',
info: {
title: 'My API',
version: 'v1',
description: 'API documentation for the platform'
},
paths: {},
servers: [
{
url: '{protocol}://{host}',
variables: {
protocol: {
default: 'https',
enum: ['http', 'https']
},
host: {
default: 'www.example.com'
}
}
}
],
components: {
securitySchemes: {
api_key: {
type: :apiKey,
name: 'Authorization',
in: :header,
description: 'API key for endpoints'
},
},
schemas: {
Error: {
type: :object,
properties: {
error: { type: :string },
status: { type: :string }
}
},
Person: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
}
}
}
}
}
}
config.openapi_format = :yaml
end
Writing Endpoint Specs
Each API endpoint gets documented in a spec file under spec/requests/api/. Here’s an example:
require 'swagger_helper'
RSpec.describe 'Api::V1::People', type: :request do
path '/api/v1/people/{id}' do
parameter name: :id, in: :path, type: :integer, description: 'Person ID'
get 'Retrieve a person with hobbies' do
tags 'People'
description 'Returns person information and their hobbies'
produces 'application/json'
response '200', 'person found' do
schema type: :object,
properties: {
person: { '$ref' => '#/components/schemas/Person' },
hobbies: { type: :array, items: { type: :string } }
}
let(:person) { Fabricate(:person) }
let(:id) { person.id }
run_test!
end
response '404', 'person not found' do
schema '$ref' => '#/components/schemas/Error'
let(:id) { 0 }
run_test!
end
end
end
end
This does two things: it generates OpenAPI documentation, and it runs as a real integration test. The run_test! macro makes an actual HTTP request and validates the response against the schema.
After writing specs, we generate the YAML file:
rake rswag:specs:swaggerize
This creates swagger/v1/swagger.yaml, which Swagger UI reads.
Security Considerations
We made a few decisions to balance usability with security:
-
Admin-only access: Documentation is mounted at
/admin/api-docsbehind admin authentication. Only internal team members can browse it by default. -
Disable “Try it out” in production: The
supportedSubmitMethods: []config disables interactive testing in production. This prevents unintended API calls from the docs interface. -
Authentication in specs: By including authentication in the specs (e.g.,
security [api_key: []]), the documentation shows customers exactly how to authenticate. This reduces support questions.
What I Learned
Documentation as tests is a forcing function: Writing specs that double as documentation forces you to think about edge cases. If you can’t document a behavior clearly, it’s probably not well-designed.
Interactive docs reduce support burden: Customers can test endpoints themselves before asking questions.
Security defaults matter: Disabling “Try it out” in production was a deliberate choice. We want customers to test in their own environments, not in production from our docs.
Next Steps
We’re still iterating on this setup. A few things on the roadmap:
- Add more examples to responses (rswag supports this with the
exampleskey) - Create a public subset of the docs for customers, separate from the admin view
- Automate YAML generation in CI so docs stay in sync with code
For now, rswag gives us a lightweight, Rails-native way to document APIs. The integration with RSpec means our docs are always tested, and the Swagger UI makes it easy for customers to get started quickly.