OpenAPI
Auto-generated API reference pages from an OpenAPI spec.
~ 3 min read
OpenAPI
Point Tangly at an OpenAPI spec; every endpoint becomes a page.
Top-level setup
In docs.json:
{
"api": {
"openapi": "https://api.example.com/openapi.json",
"viewer": "tangly",
"auth": { "method": "bearer", "name": "Authorization" }
}
}Tangly fetches the spec at build time. openapi accepts a URL, a local path (relative to project root), or an array of either.
Dev override
Set TANGLY_OPENAPI_URL to point Tangly at a different spec without editing docs.json. Useful for rendering docs against a dev/staging API while keeping prod config untouched.
TANGLY_OPENAPI_URL=https://api.dev.example.com/openapi.json tangly devOr drop it in .env.local at the project root. tangly dev, tangly build, and tangly preview load .env.local then .env on startup and log the files they pick up. Shell-exported values still win over file values.
# .env.local (gitignored)
TANGLY_OPENAPI_URL=https://api.dev.example.com/openapi.jsonWith taskmux, add it to the task env in taskmux.toml:
[tasks.docs]
env = { TANGLY_OPENAPI_URL = "https://api.dev.example.com/openapi.json" }Rules:
- Unset → fallback to
api.openapiindocs.json. Prod stays unchanged. - Global: replaces every OpenAPI source in the manifest (top-level + per-tab) with the override.
- Mode-agnostic: applies to
dev,preview, andbuildif the var is exported. Don’t set it in prod CI unless you mean to. - Tangly logs
[tangly] OpenAPI source overridden via TANGLY_OPENAPI_URL=<url>on startup so the override is never silent.
Viewer choice
viewer selects the rendering engine:
| Value | Render |
|---|---|
tangly (default) | Built-in compact endpoint render. Server-fetched at build time. |
scalar | Scalar viewer, lazy-loaded from CDN. |
redoc | ReDoc viewer, lazy-loaded from CDN. |
stoplight | Stoplight Elements, lazy-loaded from CDN. |
The default tangly renderer is fastest and works without client JS. The third-party viewers add interactivity but pull bundles from CDN at runtime.
Per-endpoint pages
A page with openapi: in frontmatter auto-generates the endpoint:
---
title: Get network data
openapi: get /v4/data/network/{network_code}
---The page switches to the split layout (docs left, sticky playground panel right) and renders:
- HTTP method badge + path (color-matched to the sidebar pill)
- Summary + description from the operation
- Path / Query / Header / Cookie parameter sections (each row expandable)
- Request body schema tree
- Tabbed responses by status code, with the response schema per status
- Right-rail panel with language tabs (cURL, TypeScript, Python by default),
Send →button, and live response preview
Right-rail playground panel
Every openapi: / api: page gets a sticky panel on the right at xl+ widths. Default tabs:
| Tab | How it’s built |
|---|---|
cURL | Generated from the operation. Uses --request, embeds path/query example values, includes the configured auth header, and any application/json example body. |
TypeScript | await fetch(...) snippet. |
Python | requests.<method>(...) with explicit headers= / params=. |
The set is configurable:
{
"api": {
"codeSamples": {
"languages": ["curl", "typescript", "python", "go"],
"prefill": true,
"defaults": "required",
"autogenerate": true
}
}
}| Field | Meaning |
|---|---|
languages | Tab order. Built-in generators: curl/bash/shell, typescript/ts, javascript/js, python/py, go. Unknown names render an “author via <RequestExample>” placeholder. |
prefill | Pull example values from spec parameters into the snippet (default true). |
defaults | required (only required params, default) or all. |
autogenerate | When false, only <RequestExample> overrides and x-codeSamples show. |
Mintlify-flavored api.examples.* is accepted as an alias for api.codeSamples.* and normalized at parse time, so unmodified Mintlify projects keep working.
Per-endpoint code samples (<RequestExample>)
Override or add language samples by wrapping fenced code blocks in <RequestExample> inside the endpoint MDX:
---
openapi: get /v4/data/network/{network_code}
---
<RequestExample>
```ts
import { OpenElectricityClient } from "openelectricity";
const client = new OpenElectricityClient();
const { datatable } = await client.getNetworkData("NEM", ["energy"], {});
```
```python
from openelectricity import OEClient
client = OEClient()
client.get_network_data("NEM", ["energy"], {})
```
</RequestExample>Match rule: each fenced block’s language token (ts, python, go, …) replaces the same-language panel tab. Languages not in api.codeSamples.languages are silently ignored. The inline <RequestExample> block hides itself once the panel picks up the override — no duplicate render.
<ResponseExample> mirrors the pattern for response previews.
Spec-side samples (x-codeSamples)
If your spec already carries x-codeSamples (or the legacy x-code-samples), Tangly picks them up automatically:
paths:
/v4/data/network/{network_code}:
get:
x-codeSamples:
- lang: typescript
label: TypeScript SDK
source: |
import { OEClient } from "openelectricity";
const c = new OEClient();
await c.getNetworkData("NEM", ["energy"], {});Precedence: <RequestExample> MDX overrides ▸ x-codeSamples ▸ autogen.
Hiding endpoints
x-hidden: trueon an operation keeps the page reachable by URL but removes it from the sidebar.x-excluded: truedrops the page entirely.
Parameter pills
Surface custom OpenAPI fields as pills next to the parameter name:
{
"api": {
"params": {
"expanded": "all",
"post": ["readOnly", "deprecated"]
}
}
}expanded: "all" opens every parameter row by default; post lists field keys to render as small pills (any truthy value on the parameter or its schema is surfaced).
Companion narrative
Add MDX content alongside the auto-rendered endpoint by setting api: instead of openapi::
---
title: Get network data
api: GET /v4/data/network/{network_code}
---
# Background
This endpoint returns time-series data sliced by network. Cache for at least 60s; backend regenerates the underlying view every minute.
## Common pitfalls
- Pass `interval=5m` for production dashboards.
- Time ranges over 30 days are rate-limited.Tangly merges your narrative with the auto-generated endpoint render. Both stay in sync as the spec evolves.
Auto-generated tab
Set openapi on a tab node to materialize one page per endpoint:
{
"tab": "API Reference",
"openapi": "./openapi.json"
}Tangly walks the spec, generates a synthetic PageEntry for each operation, and appends them to the tab’s sidebar grouped by tag.
Authentication
Configure auth at the top level so try-it-out forms include credentials:
{
"api": {
"auth": { "method": "bearer", "name": "Authorization" }
}
}method | Behavior |
|---|---|
bearer | Adds Authorization: Bearer <token> to requests. Token comes from a per-page input. |
basic | Adds Authorization: Basic <base64>. |
key | Adds <name>: <value> header (or query param if the spec says so). |
none | No auth. |
Multi-environment base URLs
{
"api": {
"baseUrl": ["https://api.example.com", "https://staging-api.example.com"]
}
}The try-it-out form gets a server selector.
Local specs
Local OpenAPI files are resolved relative to project root:
{ "api": { "openapi": "./openapi.json" } }The Vite static-asset middleware doesn’t auto-serve OpenAPI files; they’re loaded server-side at build time, not via HTTP.
Validation
tangly check validates your spec via @apidevtools/swagger-parser:
bun x tangly check --strictBad refs, missing schemas, malformed operations all surface as errors before you build.