OpenAPI bridge
If you already maintain an OpenAPI spec, don't restate it. Point c.openapi at the file and one line renders the whole endpoint — method, path, fields, errors, request tabs, and response — derived from the spec.
Zero hand-restatement#
One operation call → a full endpoint reference, composed from the kit.
An API-reference page normally restates every method, path, field, and response that your openapi.yaml already describes — and a field rename means editing both. The OpenAPI bridge closes that gap: a single operation "operationId" reads the spec and renders the whole endpoint through the same kit the API reference page composes by hand — a DocsUI::Endpoint badge, FieldTables, an ErrorTable, a RequestExample (or your x-codeSamples), and a JsonResponse.
Because it's composed from the kit, the .md twin, llms.txt, search, and MCP surfaces all derive from it for free.
Point c.openapi at your spec#
A path (.json ⇒ JSON, else YAML) or an already-parsed Hash. nil by default.
DocsKit.configure do |c|
c.openapi = Rails.root.join("openapi.yaml")
endThen, in any page's #content, render an operation by its operationId:
def content
operation "createInvoice"
endopenapi.yaml in development shows up without a server restart.What one operation expands to#
Each part of the operation maps to one kit component.
| Spec source | Renders as |
|---|---|
| operationId + summary + method/path | the Section title + a DocsUI::Endpoint badge |
| description | Markdown prose |
| parameters (query/path) | a DocsUI::FieldTable |
| requestBody schema ($ref, allOf, nested) | a FieldTable (nested names dotted: customer.id) |
| 4xx / 5xx responses | a DocsUI::ErrorTable (error type from a response example) |
| x-codeSamples | DocsUI::Example tabs (a lone sample → a plain Code) |
| no code samples | a generated DocsUI::RequestExample |
| first 2xx example | a DocsUI::JsonResponse |
Live: createInvoice#
Rendered from docs/openapi.yaml — request body, errors, tabs, and response.
The section below is produced by a single call — operation "createInvoice". Nothing here is hand-written:
Create an invoice#
POST /v1/invoices
Creates a new invoice for a customer and returns it.
| Name | Type | Required | Description |
|---|---|---|---|
id | string | — | The unique invoice identifier. |
amount | integer | ✓ | Amount due, in the smallest currency unit (e.g. cents). |
currency | usd | eur | gbp | ✓ | Three-letter ISO currency code. |
customer | object | — | The customer the invoice is billed to. |
customer.id | string | — | |
customer.email | string | — | |
created | integer | — | Unix timestamp of creation. |
| Scenario | Status | Type |
|---|---|---|
| Missing or invalid API key. | 401 | authentication_error |
| The invoice failed validation. | 422 | validation_error |
curl -X POST 'https://api.example.com/v1/invoices' \
-H "Content-Type: application/json" \
-d '{
"id": "inv_1a2b3c",
"amount": 4200,
"currency": "usd",
"customer": {
"id": "cus_9f8e7d",
"email": "[email protected]"
},
"created": 1720000000
}'const response = await fetch("https://api.example.com/v1/invoices", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({"id":"inv_1a2b3c","amount":4200,"currency":"usd","customer":{"id":"cus_9f8e7d","email":"[email protected]"},"created":1720000000}),
});
const data = await response.json();require "net/http"
require "json"
uri = URI("https://api.example.com/v1/invoices")
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request.body = {
"id": "inv_1a2b3c",
"amount": 4200,
"currency": "usd",
"customer": {
"id": "cus_9f8e7d",
"email": "[email protected]"
},
"created": 1720000000
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
endimport requests
response = requests.post(
"https://api.example.com/v1/invoices",
headers={"Content-Type": "application/json"}, json={
"id": "inv_1a2b3c",
"amount": 4200,
"currency": "usd",
"customer": {
"id": "cus_9f8e7d",
"email": "[email protected]"
},
"created": 1720000000
},
)
data = response.json(){
"id": "inv_1a2b3c",
"amount": 4200,
"currency": "usd",
"customer": {
"id": "cus_9f8e7d",
"email": "[email protected]"
},
"created": 1720000000
}Live: getInvoice (with x-codeSamples)#
This operation ships x-codeSamples, so they replace the generated tabs.
getInvoice carries x-codeSamples (a Ruby SDK tab and a CLI tab) in the spec, so the bridge renders those instead of the generic curl/JS/Ruby/Python snippets — and substitutes the id parameter's example into the path:
Retrieve an invoice#
GET /v1/invoices/{id}
Fetches a single invoice by its ID.
| Name | Type | Required | Description |
|---|---|---|---|
id | string | ✓ | The invoice ID. |
| Scenario | Status | Type |
|---|---|---|
| Missing or invalid API key. | 401 | authentication_error |
Billing::Invoice.retrieve("inv_1a2b3c")billing invoices retrieve inv_1a2b3c{
"id": "inv_1a2b3c",
"amount": 4200,
"currency": "usd",
"customer": {
"id": "cus_9f8e7d",
"email": "[email protected]"
},
"created": 1720000000
}Live: listInvoices (filter the tabs)#
A GET with query parameters; here we keep just the curl and Ruby tabs.
Pass clients: to filter and order the generated client tabs — here clients: %i[curl ruby]:
List invoices#
GET /v1/invoices
Returns a paginated list of invoices, most recent first.
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | — | Maximum number of invoices to return (1–100). |
status | open | paid | void | — | Filter by invoice status. |
curl -X GET 'https://api.example.com/v1/invoices?limit=20'require "net/http"
require "json"
uri = URI("https://api.example.com/v1/invoices?limit=20")
request = Net::HTTP::Get.new(uri)
request["Content-Type"] = "application/json"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end[
{
"id": "inv_1a2b3c",
"amount": 4200,
"currency": "usd",
"customer": {
"id": "cus_9f8e7d",
"email": "[email protected]"
},
"created": 1720000000
}
]Lookup, prose, and errors#
By id or verb+path; append prose with a block; unknown ids raise.
operation :delete, "/v1/invoices/{id}" # verb + path (id-less specs)
operation "createInvoice", clients: %i[curl ruby] # only these tabs
operation "createInvoice" do |op| # append prose in the section
op.md("Idempotency keys are honored for 24 hours.")
endoperationId raises DocsKit::OpenApi::OperationNotFound (naming the available ids); an external/remote $ref raises DocsKit::OpenApi::UnsupportedRef. Authoring or validating the spec itself is out of scope — bring your own.