Undocumented Rails APIs cost teams hours per week in Slack questions, incorrect integrations, and onboarding friction. OpenAPI gives you a machine-readable spec that generates interactive docs, client SDKs, and mock servers from a single source of truth.
This guide covers the two main Rails tools for OpenAPI documentation, their real-world trade-offs, and the CI setup that prevents spec drift.
Why Most Rails API Docs Go Stale (and How to Prevent It)
The core problem is not generating docs. It is keeping them accurate after the initial setup. Developers add parameters, change response shapes, and forget to regenerate the spec. Within weeks, your OpenAPI file describes an API that no longer exists.
The fix: tie documentation generation to your test suite and CI pipeline so that outdated specs break the build.
Rails 7 #131 API Documentation with OpenAPI, Swagger, RSWAG. Generate manifest with ChatGPT?!

RSwag: Test-Driven OpenAPI Specs
RSwag (v2.14.0+) generates OpenAPI 3.0 specs from RSpec request specs. Every documented endpoint is also a tested endpoint. If the response shape changes, the test fails.
Setup (under 10 minutes)
Add to your Gemfile:
group :development, :test do
gem 'rswag', '~> 2.14'
gem 'rspec-rails', '~> 7.1'
end
Install and generate config:
bundle install
rails generate rswag:install
This creates spec/swagger_helper.rb where you define your API metadata:
# spec/swagger_helper.rb
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'
},
servers: [
{ url: 'https://api.example.com' }
]
}
}
end
Writing Your First Spec
# spec/requests/orders_spec.rb
require 'swagger_helper'
RSpec.describe 'Orders API', type: :request do
path '/api/v1/orders' do
get 'List orders' do
tags 'Orders'
produces 'application/json'
parameter name: :page, in: :query, type: :integer, required: false
parameter name: :per_page, in: :query, type: :integer, required: false
response '200', 'orders listed' do
schema type: :array, items: {
type: :object,
properties: {
id: { type: :integer },
total_cents: { type: :integer, example: 12550 },
currency: { type: :string, example: 'CHF' },
status: { type: :string, enum: %w[pending confirmed shipped] }
},
required: %w[id total_cents currency status]
}
let(:page) { 1 }
let(:per_page) { 25 }
run_test!
end
response '401', 'unauthorized' do
run_test!
end
end
end
end
Generate the spec file:
bundle exec rake rswag:specs:swaggerize
Output: swagger/v1/swagger.yaml
OasRails: Comment-Driven Docs Without Test Overhead
OasRails generates OpenAPI 3.1 specs from YARD-style annotations in your controllers. No extra test files needed.
Setup
gem 'oas_rails'
# config/routes.rb
Rails.application.routes.draw do
mount OasRails::Engine => '/docs' if Rails.env.development?
end
Annotating Controllers
class OrdersController < ApplicationController
# @summary List orders
# @parameter page [Integer] Page number (query)
# @parameter per_page [Integer] Items per page (query)
# @response 200 [Array<Order>] Paginated list of orders
# @response_example 200 application/json
# [{"id": 1, "total_cents": 12550, "currency": "CHF", "status": "confirmed"}]
def index
# implementation
end
end
Visit http://localhost:3000/docs to see live documentation.
For production, export a static file:
rails oas_rails:export
RSwag vs OasRails: When to Use Which
| Factor | RSwag | OasRails |
|---|---|---|
| OpenAPI version | 3.0.x | 3.1.0 |
| Docs generation | From RSpec tests | From YARD comments |
| Spec drift protection | Tests fail if response changes | None (comments can go stale) |
| Setup time | ~10 min | ~5 min |
| Maintenance effort | Medium (specs must stay in sync) | Low initial, high long-term |
| Best for | APIs with existing test suites | Internal APIs, prototypes |
| CI integration | Native (runs with test suite) | Requires custom validation step |
Bottom line: If you already write request specs, RSwag adds docs for near-zero extra effort. If you need quick docs for an internal tool and don’t have RSpec request specs, OasRails gets you there faster.
USEO’s Take
We have used RSwag on 8+ client projects and OasRails on 2. Here is what we have learned:
RSwag’s biggest pitfall is silent spec drift. Developers run rspec but forget rake rswag:specs:swaggerize. Tests pass, the app ships, and the swagger.yaml still describes last month’s API. We now enforce this in CI with a diff check (see the pipeline below).
OasRails comments rot faster than you expect. YARD annotations are not validated at runtime. A developer renames a parameter in the method signature but leaves the old @parameter tag. Nothing breaks. The docs silently become wrong. For production APIs consumed by external partners, this is a dealbreaker.
Our recommendation: Use RSwag for any API that has external consumers or will outlive a single sprint. Use OasRails for internal dashboards and admin APIs where speed matters more than long-term accuracy.
One thing we wish RSwag did better: Shared schema definitions. If you have 15 endpoints returning the same User object, you end up duplicating the schema in every spec file. We work around this with shared contexts:
# spec/support/schemas/user_schema.rb
RSpec.shared_context 'user_schema' do
let(:user_schema) do
{
type: :object,
properties: {
id: { type: :integer },
email: { type: :string, format: :email },
name: { type: :string },
role: { type: :string, enum: %w[admin member guest] }
},
required: %w[id email name role]
}
end
end
Then include it in your request specs:
include_context 'user_schema'
response '200', 'user found' do
schema user_schema
run_test!
end
CI Pipeline That Catches Stale Docs
This is the pipeline we run on every pull request. It regenerates the OpenAPI spec and fails if the committed version differs from the freshly generated one.
# .github/workflows/api-docs.yml
name: API Documentation Check
on: [pull_request]
jobs:
verify-docs:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Setup DB
run: bundle exec rails db:create db:schema:load
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
- name: Generate OpenAPI spec
run: bundle exec rake rswag:specs:swaggerize
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
- name: Check for spec drift
run: |
if ! git diff --exit-code swagger/; then
echo "ERROR: OpenAPI spec is out of date."
echo "Run 'rake rswag:specs:swaggerize' and commit the result."
exit 1
fi
This single step has prevented stale docs on every project where we have deployed it. No exceptions.
Code-First vs Design-First: Pick Based on Your Constraints
Code-first (RSwag, OasRails) works when:
- The API already exists and needs docs retroactively
- Your team iterates fast and the spec changes weekly
- You have a strong test suite
Design-first (Bump.sh, Stoplight Studio) works when:
- Multiple teams need to agree on the API contract before building
- External partners need to start integrating before your backend is ready
- Regulatory review requires an approved spec before development
Many teams combine both: design-first for the initial contract, then switch to RSwag for ongoing maintenance once the API stabilizes.
Mock Servers and SDK Generation from Your Spec
Once you have a valid OpenAPI spec, you unlock two powerful workflows:
Mock servers with Prism:
npx @stoplight/prism-cli mock swagger/v1/swagger.yaml
Frontend teams can build against realistic responses while the backend is still in progress. This eliminates the “waiting for the API” bottleneck.
Client SDK generation:
npx @openapitools/openapi-generator-cli generate \
-i swagger/v1/swagger.yaml \
-g typescript-axios \
-o ./sdk/typescript
Supported generators include Ruby, Python, TypeScript, Go, Java, and 40+ others.
FAQs
RSwag tests pass but the swagger.yaml is outdated. What happened?
Running rspec alone does not regenerate the OpenAPI spec file. You must run rake rswag:specs:swaggerize separately. Add the CI diff check from this article to catch this automatically.
Can I use RSwag and OasRails in the same project?
Technically yes, but it creates confusion about which tool is the source of truth. Pick one per API version. If you have a public API (RSwag) and an internal admin API (OasRails), that split can work.
How do I handle localized response formats (dates, currencies) in the spec?
Use the example field in your schema properties. Define the format once in a shared schema file and reference it across endpoints. Do not scatter locale-specific examples through every spec file.