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::Section`s (see [Authoring pages](https://docs-kit.zoolutions.llc/docs/authoring)). 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](https://docs-kit.zoolutions.llc/docs/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:`.

```ruby
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/customers

POST

/v1/customers

PATCH

/v1/customers/:id

DELETE

/v1/customers/:id

> **Note:** 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.

```ruby
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.

```ruby
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" }
])
```

> **Tip:** 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.

```ruby
render DocsUI::RequestExample.new(
  method: :post,
  path: "/v1/customers",
  body: { email: "ada@example.com", 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.

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `method: / path:` | Symbol/String, String | — | The verb and path; path is appended to c.api_base_url. |
| `body:` | Hash, String, nil | nil | Payload — 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>, nil | all configured | Filter AND order the tabs, e.g. %i[curl ruby]. |

> **Warning:** 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](https://docs-kit.zoolutions.llc/docs/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).

```ruby
DocsKit.configure do |c|
  c.api_base_url    = "https://api.acme.com"
  c.api_auth_header = "Authorization: Bearer sk_live_..."
end
```

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `c.api_base_url` | String | "https://api.example.com" | Host prefixed onto every RequestExample path. |
| `c.api_auth_header` | String, nil | nil | Example Authorization line merged into every snippet. |
| `c.api_clients` | Hash | {} | 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.

```ruby
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
```

> **Note:** 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:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | ✓ | The customer's email address. |
| `name` | string | — | The customer's full name. |
| `metadata` | object | — | Up to 50 key/value pairs, e.g. `plan: pro`. |

| Scenario | Status | Type | Param |
| --- | --- | --- | --- |
| Missing or invalid API key | 401 | `authentication_error` | — |
| Email already registered | 422 | `validation_error` | `email` |

Try it — one declaration renders every client tab:

cURL

JavaScript

Ruby

Python

```console
curl -X POST 'https://api.example.com/v1/customers' \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "metadata": {
    "plan": "pro"
  }
}'
```

```javascript
const response = await fetch("https://api.example.com/v1/customers", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({"email":"ada@example.com","name":"Ada Lovelace","metadata":{"plan":"pro"}}),
});
const data = await response.json();
```

```ruby
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": "ada@example.com",
  "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
```

```python
import requests

response = requests.post(
    "https://api.example.com/v1/customers",
    headers={"Content-Type": "application/json"}, json={
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "metadata": {
    "plan": "pro"
  }
},
)
data = response.json()
```

A successful response:

```json
{
  "id": "cus_1a2b3c",
  "object": "customer",
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "metadata": {
    "plan": "pro"
  },
  "created": 1720000000
}
```

The calls that produced the block above:

```ruby
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: "ada@example.com", name: "Ada Lovelace" }
  )
  render DocsUI::JsonResponse.new(
    { id: "cus_1a2b3c", object: "customer", email: "ada@example.com" }
  )
end
```

> **Tip:** See the [Components](https://docs-kit.zoolutions.llc/docs/components) reference for every arg of every kit component, and [Markdown authoring](https://docs-kit.zoolutions.llc/docs/markdown) for the `md` prose used throughout this page.