Authoring

API reference

Declare an endpoint once — method, path, fields, one request — and the kit renders the badge, the tables, and a code tab per client. Base URL, auth, and the client set are config.

The API-reference kit#

Six components compose a full endpoint reference — no hand-rolled badges or per-language heredocs.

An API-reference page is the same page you'd write for anything else — a Phlex class of DocsUI::Sections (see Authoring pages). What's different is the vocabulary you reach for inside #content:

  • DocsUI::Endpoint — a verb badge + monospace path, inline;
  • DocsUI::FieldTable / DocsUI::ErrorTable — schema-driven tables;
  • DocsUI::RequestExample — one request declaration → one code tab per client;
  • DocsUI::JsonResponse — a Ruby Hash rendered as pretty JSON.

Three config knobs — api_base_url, api_auth_header, api_clients — feed every RequestExample, so a snippet is authored once and points at the right host with the right auth. The Components reference lists every arg; this page walks the workflow.

Endpoint — the method + path line#

A colour-coded verb badge and a monospace path, rendered inline.

DocsUI::Endpoint.new(method, path) renders an HTTP method badge coloured per verb — GET success, POST primary, PUT/PATCH warning, DELETE error, anything else neutral — followed by the path in a <code>. It renders inline (no wrapper), on purpose: drop it straight into a Section's description:.

DocsUI::Section("List customers",
  description: DocsUI::Endpoint.new(:get, "/v1/customers")) do
  # fields, request example, response…
end

The badge live, for each verb:

GET /v1/customersPOST /v1/customersPATCH /v1/customers/:idDELETE /v1/customers/:id
The verb → badge map is a frozen Hash of literal class strings so Tailwind's Ruby scan generates each colour — never interpolate them. The path is Phlex-escaped, and an unknown verb (e.g. :trace) degrades to a neutral badge rather than raising.

FieldTable & ErrorTable — the schemas#

Keyword-schema presets over DocsUI::Table for request fields and endpoint errors.

DocsUI::FieldTable takes an Array of field Hashes and renders Name / Type / Required / Description. name is auto code-styled, required: defaults to false (a when true, the canonical when not), and the description cell follows DocsUI::Table's convention — a plain String is escaped text, [:code, "x"] is inline code, [:md, "…"] is inline Markdown.

render DocsUI::FieldTable.new([
  { name: "email", type: "string", required: true,
    description: "The customer's email address." },
  { name: "metadata", type: "object",
    description: [:md, "Up to 50 keys, e.g. `plan: pro`."] }
])

DocsUI::ErrorTable renders Scenario / Status / Type — plus a Param column, but only when at least one error names a param:. An endpoint whose errors are all param-free renders a clean three-column table; when the column IS shown, a param-free row gets the em-dash. type and param are auto code-styled.

render DocsUI::ErrorTable.new([
  { scenario: "Missing or invalid API key", status: "401",
    type: "authentication_error" },
  { scenario: "Email already taken", status: "422",
    type: "validation_error", param: "email" }
])
The placeholder is the kit's ONE canonical "no value" glyph (an em-dash, never an ASCII hyphen) — both tables share it, so a page never types a stray -.

RequestExample — one declaration, every client#

Declare method/path/body once; get a syntax-highlighted tab per configured client.

DocsUI::RequestExample is the payoff. Declare the request once and it renders one code tab per configured client — curl, javascript, ruby, python by default — wrapped in a DocsUI::Example, so the reader's sticky global language choice persists across every endpoint on the site.

render DocsUI::RequestExample.new(
  method: :post,
  path: "/v1/customers",
  body: { email: "[email protected]", name: "Ada Lovelace" }
)

Under the hood each tab is fed a DocsKit::ApiRequest — an immutable value object carrying method/path/url/query/headers/body with display helpers (#http_method, #body?, #pretty_body_json, #url_with_query). Every shipped template guards its payload lines on #body?, so a body-less GET emits no dangling -d / json= / request.body = line.

OptionTypeDefaultDescription
method: / path:Symbol/String, StringThe verb and path; path is appended to c.api_base_url.
body:Hash, String, nilnilPayload — deep-stringified into each snippet. Omit for a GET.
query:Hash{}Query params, URL-encoded onto every snippet's URL.
headers:Hash{}Extra headers, merged over the config auth header (explicit wins).
clients:Array<Symbol>, nilall configuredFilter AND order the tabs, e.g. %i[curl ruby].
A lone client renders no tabs — DocsUI::Example needs at least two. When demoing one custom client, pair it (e.g. clients: %i[cli curl]). An unknown token in clients: is silently skipped — a typo yields fewer tabs, never a raise.

Configure the base URL & auth#

Two knobs point every snippet at your real host with a real auth line.

RequestExample reads three config knobs. Set them once in the initializer (see Configuration) and every endpoint inherits them.

  • c.api_base_url — prefixed onto every path:. Defaults to the neutral https://api.example.com.
  • c.api_auth_header — an example Authorization line merged into every snippet. Defaults to nil → no auth line (clean snippets for a no-auth API). It's split on the first colon into a { name => value } header, merged under any explicit headers: you pass (so your explicit header wins).
  • c.api_clients — override or extend the tab set (next section).
config/initializers/docs_kit.rb
DocsKit.configure do |c|
  c.api_base_url    = "https://api.acme.com"
  c.api_auth_header = "Authorization: Bearer sk_live_..."
end
OptionTypeDefaultDescription
c.api_base_urlString"https://api.example.com"Host prefixed onto every RequestExample path.
c.api_auth_headerString, nilnilExample Authorization line merged into every snippet.
c.api_clientsHash{}Overrides/extensions merged over the four defaults (writer only).

Custom clients — SDK-flavored tabs#

Replace a default tab with an SDK snippet, or append your own (a cli tab).

The gem ships generic HTTP snippets because it can't know your SDK. A DocsKit::ApiClient describes one tab — a label, a Rouge lexer, an optional filename (a String or a (request) -> String proc), and a template: a (DocsKit::ApiRequest) -> String callable that renders the snippet.

Set c.api_clients to a { token => ApiClient } Hash. It merges over the four defaults (curl, javascript, ruby, python): reusing a default token replaces that tab with an SDK-flavored one; a new token appends a tab. Hash merge preserves order — reused tokens keep their slot, new ones append in declaration order.

config/initializers/docs_kit.rb
DocsKit.configure do |c|
  c.api_clients = {
    # Replace the generic Ruby tab with the SDK flavour:
    ruby: DocsKit::ApiClient.new(
      label: "Ruby", lexer: :ruby, filename: "acme.rb",
      template: ->(req) { %(Acme.#{req.http_method.downcase}("#{req.path}")) }
    ),
    # Append a brand-new CLI tab:
    cli: DocsKit::ApiClient.new(
      label: "Acme CLI", lexer: :shell, filename: "cli.sh",
      template: ->(req) { "acme #{req.http_method.downcase} #{req.path}" }
    )
  }
end
Read the effective map via DocsKit.configuration.api_clients (which merges over the defaults), never the raw ivar. Because it merges, setting one cli: client yields FIVE tabs, not one. Config can't remove a default tab — use RequestExample's clients: to select a subset.

A full endpoint, live#

POST /v1/customers

Everything above, composed — an Endpoint description, a FieldTable, an ErrorTable, a RequestExample (four real client tabs, on THIS site's config), and a JsonResponse. This is the real kit rendering, not a screenshot:

NameTypeRequiredDescription
emailstringThe customer's email address.
namestringThe customer's full name.
metadataobjectUp to 50 key/value pairs, e.g. plan: pro.
ScenarioStatusTypeParam
Missing or invalid API key401authentication_error
Email already registered422validation_erroremail

Try it — one declaration renders every client tab:

request.sh
curl -X POST 'https://api.example.com/v1/customers' \
  -H "Content-Type: application/json" \
  -d '{
  "email": "[email protected]",
  "name": "Ada Lovelace",
  "metadata": {
    "plan": "pro"
  }
}'
request.js
const response = await fetch("https://api.example.com/v1/customers", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({"email":"[email protected]","name":"Ada Lovelace","metadata":{"plan":"pro"}}),
});
const data = await response.json();
request.rb
require "net/http"
require "json"

uri = URI("https://api.example.com/v1/customers")
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request.body = {
  "email": "[email protected]",
  "name": "Ada Lovelace",
  "metadata": {
    "plan": "pro"
  }
}.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/customers",
    headers={"Content-Type": "application/json"}, json={
  "email": "[email protected]",
  "name": "Ada Lovelace",
  "metadata": {
    "plan": "pro"
  }
},
)
data = response.json()

A successful response:

response.json
{
  "id": "cus_1a2b3c",
  "object": "customer",
  "email": "[email protected]",
  "name": "Ada Lovelace",
  "metadata": {
    "plan": "pro"
  },
  "created": 1720000000
}

The calls that produced the block above:

DocsUI::Section("Create a customer",
  description: DocsUI::Endpoint.new(:post, "/v1/customers")) do
  render DocsUI::FieldTable.new([
    { name: "email", type: "string", required: true,
      description: "The customer's email address." }
  ])
  render DocsUI::ErrorTable.new([
    { scenario: "Email already registered", status: "422",
      type: "validation_error", param: "email" }
  ])
  render DocsUI::RequestExample.new(
    method: :post, path: "/v1/customers",
    body: { email: "[email protected]", name: "Ada Lovelace" }
  )
  render DocsUI::JsonResponse.new(
    { id: "cus_1a2b3c", object: "customer", email: "[email protected]" }
  )
end
See the Components reference for every arg of every kit component, and Markdown authoring for the md prose used throughout this page.