Rails API documentation, generated from the specs that already run
September 22, 2026
An API's documentation is wrong the moment somebody adds a field and does not open the Markdown file. Every hand-written API reference has that failure built into it, and the interval between the change and the correction is invisible: the page still renders, the client library still compiles, and the first person to find out is a customer reading a field that no longer exists.
Generating the reference from something that already has to be correct is the way out. In Rails, the only artefact in the repository that fails loudly when the API changes is the request spec.
What Rails itself gives you, which is a route table
Rails has no API documentation generator in it. Nothing in Action Pack, Action Dispatch or the Rails command line emits an OpenAPI document, a Swagger page or anything else a client can read.
What Rails does ship is bin/rails routes, and knowing its limits is the reason the rest of this page exists. The routing guide documents --expanded for a vertical layout, -g to grep "any routes that partially match the URL helper method name, the HTTP verb, or the URL path", -c to filter by controller, and --unused for routes nothing points at. Run bin/rails routes -g api against this boilerplate and you get three lines: a POST to api/v1/sessions#create, and a GET and a PATCH to api/v1/users#show and #update.
Three lines is the whole of what Rails knows. The route table holds the path, the verb, the helper and the controller action. It does not hold the parameters that action accepts, the JSON it renders, the status codes it can answer, or the fact that every one of those endpoints demands an Authorization: Bearer header. A route table tells a client where to knock. Documentation tells it what to say and what will come back.
RDoc and YARD do not close the gap either, because both document Ruby classes rather than HTTP. A comment above def update describes a method. A client integrating over the network never sees that method; it sees a URL, a body and a status code.
Three Rails API documentation gems that still exist in 2026
Three gems cover this, and their states differ enough that the choice is mostly made for you. Every figure below was read from RubyGems and GitHub on 2026-09-22.
Gem
Latest
Released
How it is written
rswag
2.17.0
2025-11-05
A Swagger DSL inside RSpec, replacing the request spec
apipie-rails
1.5.1
2026-06-26
A DSL inside the controller
rspec-openapi
0.34.0
2026-09-22
No DSL; records the request specs you already have
rswag is the name most people reach for, and the one to check before adopting. Its README opens with "Seeking maintainers! Got a pet-bug that needs fixing? Just let us know in your issue/pr that you'd like to step up to help." The repository is not archived and was pushed to on 2026-08-24, so it is not dead; the last cut release is ten months old. The README still advertises "OpenApi 3.0 compatible!", two minor versions of the specification behind. rswag is three gems that install together or apart: rswag-specs for the DSL and the rake task, rswag-api for an engine that serves the generated file, and rswag-ui for an engine carrying Swagger UI.
apipie-rails is the oldest of the three, with 1.5.1 cut on 2026-06-26 and 232 open issues. Documentation lives in the controller: resource_description, then api :GET, '/users/:id', param :id, :number, desc: 'id of the requested user' and returns code: 200. The DSL does double duty, because validate defaults to :implicitly and wraps your actions in runtime parameter validation derived from the same declarations. The disqualifying line is in its own README: "Only OpenAPI 2.0 is supported", emitted by rake apipie:static_swagger_json[2.0]. Writing a new API in 2026 against Swagger 2.0 is choosing a format that predates every tool you will want to point at it.
rspec-openapi is the one that changes the shape of the problem, because it asks for no DSL. Its pitch is the whole argument: existing gems "require a special DSL specific to these gems, and we can't reuse existing request specs as they are."
The position: the documentation is a by-product of the test suite
Documentation generated from the tests that already run is the only kind that stays true, and the reason is not tooling quality. A hand-written reference and a controller DSL are both second descriptions of the same behaviour, and a second description drifts because nothing breaks when it does. A request spec is a third thing: it drifts and the build goes red.
With rspec-openapi installed, the mechanics are one environment variable. OPENAPI=1 bundle exec rspec runs the suite as usual and writes doc/openapi.yaml. What gets recorded is the actual request and the actual response, not what the example asserted about them, which matters more than it sounds: a spec that only checks have_http_status(:ok) still contributes the full response body to the schema.
Here is the price, and take it seriously before adopting. Your documentation coverage collapses into your test coverage. An endpoint no spec calls does not appear. A status code no spec provokes does not appear. A field that is nil in every factory is typed as null. The fix for a missing paragraph stops being "write the paragraph" and becomes "write the spec", which is more work and lands in a file reviewed by people who thought they were reviewing tests.
What would change this position: an API with a contract negotiated before the code exists. Design-first teams write the OpenAPI document as the specification and validate the implementation against it, with committee (5.6.4) or openapi_first (4.0.0) as the middleware that rejects a non-conforming request. That is a different discipline and a better one when several teams consume the API. Generating from specs is the right answer for the far more common case: one team, an API that already exists, and a document that does not.
What this boilerplate's API request specs already assert
The raw material any generator would consume is two files. spec/requests/api/v1/sessions_spec.rb covers the token endpoint with three examples: valid confirmed credentials return 201 with a token that Api::Auth::VerifyToken resolves back to the user, a wrong password returns 401, and a correct password on an unconfirmed account returns 401.
spec/requests/api/v1/users_spec.rb covers the profile endpoint with two examples plus two shared ones:
describe"GET /api/v1/user"dolet(:path){"/api/v1/user"}let(:feature){:api}it_behaves_like"a token-authenticated endpoint",:getit_behaves_like"a feature-flagged route"it"returns the token holder"doget"/api/v1/user",headers: headersexpect(response).tohave_http_status(:ok)expect(response.parsed_body).toinclude("email_address"=>user.email_address,"name"=>"Jane")endend
"a token-authenticated endpoint" fires three requests per verb: no header at all, Bearer not-a-real-token, and a freshly issued token. "a feature-flagged route" sets Setting.current.features to { "api" => false } and expects 404.
Recorded, that produces a usable document. POST /api/v1/session with a 201 carrying token and a nested user object, and a 401 carrying {"error": "Invalid email address or password."}. GET /api/v1/user with a 200 carrying the seven fields Api::V1::UserSerializer exposes, a 401, and a 404. PATCH /api/v1/user with a 200 and a 401. Thirteen recorded responses across three endpoints, from specs written with no thought of documentation at all, which is the argument in one paragraph. The API those specs describe is the one taken apart in the JWT API authentication article; the JSON API hub is where the rest of the endpoint's behaviour lives.
The 422 nobody would ever see
Api::V1::UsersController#update has two branches and the suite only ever takes one:
The failure branch renders a different shape, an errors array of strings rather than a user object, under status 422. No example in spec/requests/api/v1/users_spec.rb sends a payload that fails validation, so a generated document would describe PATCH /api/v1/user as an endpoint that answers 200 or 401 and never mentions 422. A client written against that document has no branch for the response it will actually get the first time somebody submits a blank name.
The same hole sits under the login endpoint from the other direction: sessions_spec.rb does not include "a feature-flagged route", so nothing asserts that POST /api/v1/session answers 404 when the api feature is switched off in the admin. The controller does answer 404 there, because gated_by :api installs a prepended callback that skip_before_action :authenticate_api_user!, only: :create cannot remove, which a disabled feature answering 404 works through. Real behaviour, no spec, therefore no documentation.
Both gaps existed before anyone thought about documentation. Pointing a recorder at the suite is what made them legible, and that is the strongest thing to be said for the approach: an undocumented response is a missing test wearing a different hat.
Two 401s with different bodies
A generated document catches something a hand-written one nearly always smooths over. This API answers 401 in two places and the bodies are not the same.
Api::V1::BaseController rejects a bad token with head :unauthorized, which sends an empty body. Api::V1::SessionsController#create rejects bad credentials with render json: { error: I18n.t("api.errors.invalid_credentials") }, status: :unauthorized, which sends {"error": "Invalid email address or password."}. Singular error, a string, versus nothing at all, versus the plural errors array the 422 branch renders.
Nobody writing a reference page by hand documents three error shapes for one small API, because the page would say "errors are returned as JSON" and move on. A recorder writes down what came back, so the inconsistency ends up in the schema where a client author has to look at it. Whether the right response is to unify the shapes or to document them as they are is a judgement; being unable to avoid the question is the value.
What rswag would cost this codebase specifically
Adopting rswag here is not an install, it is a rewrite of the two spec files. Its DSL replaces the RSpec you have: path '/api/v1/user' do, get 'Returns the token holder' do, a response '200', 'ok' do, and run_test! at the bottom to actually issue the request.
The concrete loss is the shared examples. "a token-authenticated endpoint" and "a feature-flagged route" are parameterised RSpec shared examples applied across this codebase's endpoints, and rswag's path/response blocks have no way to take one. Every 401 case and every 404-when-disabled case would be hand-written per endpoint, per verb, in the documentation DSL, which is exactly the duplication the shared examples were extracted to remove. A boilerplate whose whole idea is a globalised contract per endpoint pays twice for it.
The gain rswag offers over a pure recorder is real and worth naming: the descriptions are authored, so summaries, parameter descriptions and named examples say what a human meant rather than what a factory happened to produce. With rswag-ui you also get Swagger UI mounted in the app, which is a demo page for free. Weighed against a DSL rewrite, a ten-month-old release and a README asking for maintainers, that is not enough here.
The noise a recorder makes, and the four settings that quiet it
Running OPENAPI=1 bundle exec rspec on this codebase's whole suite is the wrong turn, and it is the first thing anybody does. spec/requests holds 48 spec files and exactly 2 of them are the API. The other 46 exercise the HTML side: sign-in pages, onboarding, checkout, admin. A recorder does not know the difference, so the first doc/openapi.yaml describes /session/new and /admin/users as if they were API endpoints, in a file called the API documentation.
Four settings turn the output back into something reviewable:
enable_example is the churn control. Examples are recorded from whatever the factories produced, so a user id and a generated email address land in the YAML and change on every run, which turns doc/openapi.yaml into a file with a diff in every pull request and no information in any of them. Turning examples off costs readability, and the alternative is deterministic factory data, which is a bigger project than it sounds.
security_schemes has to be declared because no recorder can infer it. The gem sees a request with an Authorization header; it cannot know the token is an HS256 JWT issued by POST /api/v1/session and valid for 24 hours. Neither can it record the feature flag: an endpoint that 404s when an operator toggles api off is an operational fact with no request that demonstrates it unless a spec sets the flag, which here only one of them does.
Pinning openapi_version prevents a surprise that reads like a bug and is not. The generated version defaults to 3.2.0, the specification the OpenAPI Initiative announced in September 2025, and the README warns that "an existing file is read regardless of its version, so the first OPENAPI=1 run after upgrading rewrites it" from 3.0.3 to 3.2.0, turning every nullable: true into a null entry in a type array. A routine bundle update followed by a doc regeneration produces a thousand-line diff nobody asked for.
Checking a generated schema in CI, where there is no comfortable answer
CI is the last decision and the one with no clean option. Regenerate doc/openapi.yaml in CI and fail the build on a diff, and every legitimate response change is a red build until somebody commits the regenerated YAML, which trains people to commit it without reading it. Skip the check entirely and the file goes stale exactly the way the hand-written page did, which was the whole reason for generating it.
The version worth running is a scheduled job rather than a gate: regenerate nightly, open a pull request when the file moved, and let a human read the diff without a merge blocked behind it. Drift stays visible and nothing goes red on a Friday afternoon because a serializer gained a field.
LaunchKit does not ship an API documentation generator
Stating the gap plainly: there is no Rails API documentation generator in this boilerplate. No rswag, no apipie-rails, no rspec-openapi, no openapi.yaml. The Gemfile's API-related line is gem "jwt" and nothing else. A buyer expecting to run one command and get a Swagger page will not find it.
What ships instead is a hand-written Markdown page, engines/boilerplate_documentation/docs/api.md, served by a local-only engine at /boilerplate/documentation. The page carries the base controller, a table of Api::Auth::IssueToken and Api::Auth::VerifyToken, the three endpoints as a fenced block, a curl example that pipes the token through jq, and instructions for adding an endpoint. Useful for the person extending the API; not a machine-readable contract, and subject to exactly the drift this page opened with.
The honest reason is scope. The API is three endpoints over one resource, and a generated OpenAPI document for three endpoints is a smaller win than the same effort spent elsewhere in the product. That calculation inverts the moment a buyer adds a fourth and a fifth endpoint of their own, which is the normal case, and at that point the specs to record are already there: every new controller inheriting Api::V1::BaseController gets "a token-authenticated endpoint" applied to it if the buyer follows the pattern, and those requests are the ones a recorder wants.
What this page does not cover
API versioning past the /api/v1 namespace already in the route file, which is a routing and deprecation problem rather than a documentation one.
Publishing the generated file: Redoc, Swagger UI outside rswag, Bump.sh and the rest of the hosting layer, all of which take an OpenAPI document as input and are indifferent to how it was produced.
Design-first validation, where the document is authored first and committee or openapi_first rejects requests that do not conform. Both gems are named above as the alternative position and neither is examined here.
Client SDK generation from the resulting document, and GraphQL, which has its own introspection story and none of this applies to it.
An API's documentation is wrong the moment somebody adds a field and does not open the Markdown file. Every hand-written API reference has that failure built into it, and the interval between the change and the correction is invisible: the page still renders, the client library still compiles, and the first person to find out is a customer reading a field that no longer exists.
Generating the reference from something that already has to be correct is the way out. In Rails, the only artefact in the repository that fails loudly when the API changes is the request spec.
What Rails itself gives you, which is a route table
Rails has no API documentation generator in it. Nothing in Action Pack, Action Dispatch or the Rails command line emits an OpenAPI document, a Swagger page or anything else a client can read.
What Rails does ship is
bin/rails routes, and knowing its limits is the reason the rest of this page exists. The routing guide documents--expandedfor a vertical layout,-gto grep "any routes that partially match the URL helper method name, the HTTP verb, or the URL path",-cto filter by controller, and--unusedfor routes nothing points at. Runbin/rails routes -g apiagainst this boilerplate and you get three lines: a POST toapi/v1/sessions#create, and a GET and a PATCH toapi/v1/users#showand#update.Three lines is the whole of what Rails knows. The route table holds the path, the verb, the helper and the controller action. It does not hold the parameters that action accepts, the JSON it renders, the status codes it can answer, or the fact that every one of those endpoints demands an
Authorization: Bearerheader. A route table tells a client where to knock. Documentation tells it what to say and what will come back.RDoc and YARD do not close the gap either, because both document Ruby classes rather than HTTP. A comment above
def updatedescribes a method. A client integrating over the network never sees that method; it sees a URL, a body and a status code.Three Rails API documentation gems that still exist in 2026
Three gems cover this, and their states differ enough that the choice is mostly made for you. Every figure below was read from RubyGems and GitHub on 2026-09-22.
rswag is the name most people reach for, and the one to check before adopting. Its README opens with "Seeking maintainers! Got a pet-bug that needs fixing? Just let us know in your issue/pr that you'd like to step up to help." The repository is not archived and was pushed to on 2026-08-24, so it is not dead; the last cut release is ten months old. The README still advertises "OpenApi 3.0 compatible!", two minor versions of the specification behind. rswag is three gems that install together or apart:
rswag-specsfor the DSL and the rake task,rswag-apifor an engine that serves the generated file, andrswag-uifor an engine carrying Swagger UI.apipie-rails is the oldest of the three, with 1.5.1 cut on 2026-06-26 and 232 open issues. Documentation lives in the controller:
resource_description, thenapi :GET, '/users/:id',param :id, :number, desc: 'id of the requested user'andreturns code: 200. The DSL does double duty, becausevalidatedefaults to:implicitlyand wraps your actions in runtime parameter validation derived from the same declarations. The disqualifying line is in its own README: "Only OpenAPI 2.0 is supported", emitted byrake apipie:static_swagger_json[2.0]. Writing a new API in 2026 against Swagger 2.0 is choosing a format that predates every tool you will want to point at it.rspec-openapi is the one that changes the shape of the problem, because it asks for no DSL. Its pitch is the whole argument: existing gems "require a special DSL specific to these gems, and we can't reuse existing request specs as they are."
The position: the documentation is a by-product of the test suite
Documentation generated from the tests that already run is the only kind that stays true, and the reason is not tooling quality. A hand-written reference and a controller DSL are both second descriptions of the same behaviour, and a second description drifts because nothing breaks when it does. A request spec is a third thing: it drifts and the build goes red.
With rspec-openapi installed, the mechanics are one environment variable.
OPENAPI=1 bundle exec rspecruns the suite as usual and writesdoc/openapi.yaml. What gets recorded is the actual request and the actual response, not what the example asserted about them, which matters more than it sounds: a spec that only checkshave_http_status(:ok)still contributes the full response body to the schema.Here is the price, and take it seriously before adopting. Your documentation coverage collapses into your test coverage. An endpoint no spec calls does not appear. A status code no spec provokes does not appear. A field that is
nilin every factory is typed as null. The fix for a missing paragraph stops being "write the paragraph" and becomes "write the spec", which is more work and lands in a file reviewed by people who thought they were reviewing tests.What would change this position: an API with a contract negotiated before the code exists. Design-first teams write the OpenAPI document as the specification and validate the implementation against it, with
committee(5.6.4) oropenapi_first(4.0.0) as the middleware that rejects a non-conforming request. That is a different discipline and a better one when several teams consume the API. Generating from specs is the right answer for the far more common case: one team, an API that already exists, and a document that does not.What this boilerplate's API request specs already assert
The raw material any generator would consume is two files.
spec/requests/api/v1/sessions_spec.rbcovers the token endpoint with three examples: valid confirmed credentials return 201 with a token thatApi::Auth::VerifyTokenresolves back to the user, a wrong password returns 401, and a correct password on an unconfirmed account returns 401.spec/requests/api/v1/users_spec.rbcovers the profile endpoint with two examples plus two shared ones:"a token-authenticated endpoint"fires three requests per verb: no header at all,Bearer not-a-real-token, and a freshly issued token."a feature-flagged route"setsSetting.current.featuresto{ "api" => false }and expects 404.Recorded, that produces a usable document.
POST /api/v1/sessionwith a 201 carryingtokenand a nesteduserobject, and a 401 carrying{"error": "Invalid email address or password."}.GET /api/v1/userwith a 200 carrying the seven fieldsApi::V1::UserSerializerexposes, a 401, and a 404.PATCH /api/v1/userwith a 200 and a 401. Thirteen recorded responses across three endpoints, from specs written with no thought of documentation at all, which is the argument in one paragraph. The API those specs describe is the one taken apart in the JWT API authentication article; the JSON API hub is where the rest of the endpoint's behaviour lives.The 422 nobody would ever see
Api::V1::UsersController#updatehas two branches and the suite only ever takes one:The failure branch renders a different shape, an
errorsarray of strings rather than a user object, under status 422. No example inspec/requests/api/v1/users_spec.rbsends a payload that fails validation, so a generated document would describePATCH /api/v1/useras an endpoint that answers 200 or 401 and never mentions 422. A client written against that document has no branch for the response it will actually get the first time somebody submits a blank name.The same hole sits under the login endpoint from the other direction:
sessions_spec.rbdoes not include"a feature-flagged route", so nothing asserts thatPOST /api/v1/sessionanswers 404 when theapifeature is switched off in the admin. The controller does answer 404 there, becausegated_by :apiinstalls a prepended callback thatskip_before_action :authenticate_api_user!, only: :createcannot remove, which a disabled feature answering 404 works through. Real behaviour, no spec, therefore no documentation.Both gaps existed before anyone thought about documentation. Pointing a recorder at the suite is what made them legible, and that is the strongest thing to be said for the approach: an undocumented response is a missing test wearing a different hat.
Two 401s with different bodies
A generated document catches something a hand-written one nearly always smooths over. This API answers 401 in two places and the bodies are not the same.
Api::V1::BaseControllerrejects a bad token withhead :unauthorized, which sends an empty body.Api::V1::SessionsController#createrejects bad credentials withrender json: { error: I18n.t("api.errors.invalid_credentials") }, status: :unauthorized, which sends{"error": "Invalid email address or password."}. Singularerror, a string, versus nothing at all, versus the pluralerrorsarray the 422 branch renders.Nobody writing a reference page by hand documents three error shapes for one small API, because the page would say "errors are returned as JSON" and move on. A recorder writes down what came back, so the inconsistency ends up in the schema where a client author has to look at it. Whether the right response is to unify the shapes or to document them as they are is a judgement; being unable to avoid the question is the value.
What rswag would cost this codebase specifically
Adopting rswag here is not an install, it is a rewrite of the two spec files. Its DSL replaces the RSpec you have:
path '/api/v1/user' do,get 'Returns the token holder' do, aresponse '200', 'ok' do, andrun_test!at the bottom to actually issue the request.The concrete loss is the shared examples.
"a token-authenticated endpoint"and"a feature-flagged route"are parameterised RSpec shared examples applied across this codebase's endpoints, and rswag'spath/responseblocks have no way to take one. Every 401 case and every 404-when-disabled case would be hand-written per endpoint, per verb, in the documentation DSL, which is exactly the duplication the shared examples were extracted to remove. A boilerplate whose whole idea is a globalised contract per endpoint pays twice for it.The gain rswag offers over a pure recorder is real and worth naming: the descriptions are authored, so summaries, parameter descriptions and named examples say what a human meant rather than what a factory happened to produce. With
rswag-uiyou also get Swagger UI mounted in the app, which is a demo page for free. Weighed against a DSL rewrite, a ten-month-old release and a README asking for maintainers, that is not enough here.The noise a recorder makes, and the four settings that quiet it
Running
OPENAPI=1 bundle exec rspecon this codebase's whole suite is the wrong turn, and it is the first thing anybody does.spec/requestsholds 48 spec files and exactly 2 of them are the API. The other 46 exercise the HTML side: sign-in pages, onboarding, checkout, admin. A recorder does not know the difference, so the firstdoc/openapi.yamldescribes/session/newand/admin/usersas if they were API endpoints, in a file called the API documentation.Four settings turn the output back into something reviewable:
enable_exampleis the churn control. Examples are recorded from whatever the factories produced, so a user id and a generated email address land in the YAML and change on every run, which turnsdoc/openapi.yamlinto a file with a diff in every pull request and no information in any of them. Turning examples off costs readability, and the alternative is deterministic factory data, which is a bigger project than it sounds.security_schemeshas to be declared because no recorder can infer it. The gem sees a request with anAuthorizationheader; it cannot know the token is an HS256 JWT issued byPOST /api/v1/sessionand valid for 24 hours. Neither can it record the feature flag: an endpoint that 404s when an operator togglesapioff is an operational fact with no request that demonstrates it unless a spec sets the flag, which here only one of them does.Pinning
openapi_versionprevents a surprise that reads like a bug and is not. The generated version defaults to3.2.0, the specification the OpenAPI Initiative announced in September 2025, and the README warns that "an existing file is read regardless of its version, so the firstOPENAPI=1run after upgrading rewrites it" from3.0.3to3.2.0, turning everynullable: trueinto anullentry in a type array. A routinebundle updatefollowed by a doc regeneration produces a thousand-line diff nobody asked for.Checking a generated schema in CI, where there is no comfortable answer
CI is the last decision and the one with no clean option. Regenerate
doc/openapi.yamlin CI and fail the build on a diff, and every legitimate response change is a red build until somebody commits the regenerated YAML, which trains people to commit it without reading it. Skip the check entirely and the file goes stale exactly the way the hand-written page did, which was the whole reason for generating it.The version worth running is a scheduled job rather than a gate: regenerate nightly, open a pull request when the file moved, and let a human read the diff without a merge blocked behind it. Drift stays visible and nothing goes red on a Friday afternoon because a serializer gained a field.
LaunchKit does not ship an API documentation generator
Stating the gap plainly: there is no Rails API documentation generator in this boilerplate. No rswag, no apipie-rails, no rspec-openapi, no
openapi.yaml. The Gemfile's API-related line isgem "jwt"and nothing else. A buyer expecting to run one command and get a Swagger page will not find it.What ships instead is a hand-written Markdown page,
engines/boilerplate_documentation/docs/api.md, served by a local-only engine at/boilerplate/documentation. The page carries the base controller, a table ofApi::Auth::IssueTokenandApi::Auth::VerifyToken, the three endpoints as a fenced block, acurlexample that pipes the token throughjq, and instructions for adding an endpoint. Useful for the person extending the API; not a machine-readable contract, and subject to exactly the drift this page opened with.The honest reason is scope. The API is three endpoints over one resource, and a generated OpenAPI document for three endpoints is a smaller win than the same effort spent elsewhere in the product. That calculation inverts the moment a buyer adds a fourth and a fifth endpoint of their own, which is the normal case, and at that point the specs to record are already there: every new controller inheriting
Api::V1::BaseControllergets"a token-authenticated endpoint"applied to it if the buyer follows the pattern, and those requests are the ones a recorder wants.What this page does not cover
API versioning past the
/api/v1namespace already in the route file, which is a routing and deprecation problem rather than a documentation one.Publishing the generated file: Redoc, Swagger UI outside rswag, Bump.sh and the rest of the hosting layer, all of which take an OpenAPI document as input and are indifferent to how it was produced.
Design-first validation, where the document is authored first and
committeeoropenapi_firstrejects requests that do not conform. Both gems are named above as the alternative position and neither is examined here.Client SDK generation from the resulting document, and GraphQL, which has its own introspection story and none of this applies to it.