Authoring

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.

config/initializers/docs_kit.rb
DocsKit.configure do |c|
  c.openapi = Rails.root.join("openapi.yaml")
end

Then, in any page's #content, render an operation by its operationId:

app/views/docs/pages/invoices.rb
def content
  operation "createInvoice"
end
The document is memoized and reloads when the file's mtime changes, so editing openapi.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 sourceRenders as
operationId + summary + method/paththe Section title + a DocsUI::Endpoint badge
descriptionMarkdown prose
parameters (query/path)a DocsUI::FieldTable
requestBody schema ($ref, allOf, nested)a FieldTable (nested names dotted: customer.id)
4xx / 5xx responsesa DocsUI::ErrorTable (error type from a response example)
x-codeSamplesDocsUI::Example tabs (a lone sample → a plain Code)
no code samplesa generated DocsUI::RequestExample
first 2xx examplea 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.

NameTypeRequiredDescription
idstringThe unique invoice identifier.
amountintegerAmount due, in the smallest currency unit (e.g. cents).
currencyusd | eur | gbpThree-letter ISO currency code.
customerobjectThe customer the invoice is billed to.
customer.idstring
customer.emailstring
createdintegerUnix timestamp of creation.
ScenarioStatusType
Missing or invalid API key.401authentication_error
The invoice failed validation.422validation_error
request.sh
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
}'
request.js
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();
request.rb
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)
end
request.py
import 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()
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.

NameTypeRequiredDescription
idstringThe invoice ID.
ScenarioStatusType
Missing or invalid API key.401authentication_error
Billing::Invoice.retrieve("inv_1a2b3c")
billing invoices retrieve inv_1a2b3c
response.json
{
  "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.

NameTypeRequiredDescription
limitintegerMaximum number of invoices to return (1–100).
statusopen | paid | voidFilter by invoice status.
request.sh
curl -X GET 'https://api.example.com/v1/invoices?limit=20'
request.rb
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
response.json
[
  {
    "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.")
end
An unknown operationId 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.