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?!

OpenAPI

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

FactorRSwagOasRails
OpenAPI version3.0.x3.1.0
Docs generationFrom RSpec testsFrom YARD comments
Spec drift protectionTests fail if response changesNone (comments can go stale)
Setup time~10 min~5 min
Maintenance effortMedium (specs must stay in sync)Low initial, high long-term
Best forAPIs with existing test suitesInternal APIs, prototypes
CI integrationNative (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.