The code shown in the tab. Rendered verbatim — HTML-escaped, with no
re-highlighting — so format it exactly as you want it to read, newlines (\n)
and indentation included.
API reference
Point Markline at an OpenAPI document and it generates a polished three-pane reference — resource pages, multi-language code, and a live request explorer.
Markline turns an OpenAPI 3.0/3.1 document into a full reference: a left resource nav, per-resource pages with attribute and parameter tables, dark code rails (cURL / Node / Python / Go), and an inline request explorer.
Add a spec
Drop in your OpenAPI document
Put it at api/openapi.json in your content root. A remote URL works too —
snapshot it into that file at build time so your docs never depend on a
live service.
content/
├── markline.json
└── api/
└── openapi.jsonAdd an API tab
Give a navigation tab openapi: true — its sidebar is generated from the
spec's tags and operations.
"navigation": {
"tabs": [
{ "id": "docs", "label": "Documentation", "href": "/", "match": ["__default__"], "groups": [] },
{ "id": "api", "label": "API reference", "href": "/api-reference", "match": ["/api-reference"], "openapi": true }
]
}Set the base URL
Point the explorer at your API. api.baseUrl overrides the spec's servers.
"api": { "baseUrl": "https://api.example.com", "playground": { "mode": "full", "proxy": "auto" } }What you get
The reference is per-resource: one page per tag, opening on the first resource. Each page has:
- A resource summary + an Endpoints card listing the tag's operations.
- A "The object" section — attributes derived from the schema, with types, required/optional flags, enums, and a sample JSON.
- One section per endpoint — method + path, parameters (path / query / body) from the schema, and a dark code rail with cURL, Node, Python and Go samples generated from the operation, plus the example response.
/api-reference/<tag>; the per-operation pages
remain reachable too (deep links, search). Navigation between docs and the
reference is instant — it's one app, not a separate site.Nested tags
Slash-separated tags — the convention NestJS emits from @ApiTags('store/orders')
— become a nested sidebar. No config: Markline groups by shared prefix and
title-cases each segment (with an acronym map, so fx → FX, api → API).
store/products Store
store/orders → ├── Products
store/refunds ├── Orders
admin/users └── Refunds
admin/api/keys Admin
├── Users
└── API
└── Keys
- Parent groups are nav-only accordions (expand/collapse), opening to the active resource. Each leaf tag stays a normal resource page.
- URLs are unchanged — the slug is still the whole tag, so
store/orderslives at/api-reference/store-ordersand existing links keep working. - Depth is arbitrary (
admin/api/keysnests three levels); the page title shows the leaf (Keys) with anAdmin / APIbreadcrumb. - Single-segment tags (
payments) are unchanged — plain top-level entries.
@ApiTags to get clean resource boundaries. Per-resource
MDX overlays still key off the full slug (store-orders.mdx).Code samples
Every endpoint gets a dark code rail. By default Markline generates four
samples — cURL, Node, Python, Go — from the operation itself: the Node/Python/Go
ones are SDK-style (client.<resource>.<method>(…)), inferred from the tag,
operationId, and the body / path / query parameters. You shape the rail two
ways.
Pick the generated languages
api.codeSamples chooses which generated languages appear, and in what order.
It's a subset of "curl" | "node" | "python" | "go"; omit it and all four show.
"api": {
"baseUrl": "https://api.example.com",
"codeSamples": ["curl", "node"] // just cURL + Node, in that order
}
"codeSamples": ["curl"] to show only cURL —
or author your own per-operation samples with x-codeSamples below.How generated samples are named
There's no separate "SDK name" setting — the generated snippets build their
identifiers from your spec. A sample like acme.payments.create({ … }) is
assembled from four derived tokens:
| Token | Derived from | Example |
|---|---|---|
| client | first word of info.title, lowercased, alphanumerics only (else client) | "Acme Payments API" → acme |
| resource | the operation's tag, lowercased and stripped | tag Payment Methods → paymentmethods |
| method | HTTP verb + path shape | POST→create, PUT/PATCH→update, DELETE→del, GET with {id}→retrieve, GET without→list |
| result | singularized tag | projects → project |
So the two levers on the generated rails are info.title (the client name)
and your tag names (the resource accessors). To match a real SDK exactly —
custom client name, hand-written method signatures, a language Markline doesn't
generate — reach for x-codeSamples.
Bring your own with x-codeSamples
To replace the generated rail on a specific operation, add the x-codeSamples
extension to that operation in your OpenAPI document (not in markline.json).
The older x-code-samples spelling is accepted too — Markline reads either, so
specs written for Redoc/Stripe work unchanged.
"paths": {
"/v1/payments": {
"post": {
"operationId": "createPayment",
"summary": "Create a payment",
"x-codeSamples": [
{
"lang": "ruby",
"label": "Ruby",
"source": "Acme::Payment.create(\n amount: 1000,\n currency: \"usd\",\n)"
},
{
"lang": "php",
"source": "$client->payments->create([\n 'amount' => 1000,\n 'currency' => 'usd',\n]);"
}
]
}
}
}
Each entry has three fields:
sourcestringrequiredlabelstringThe tab label. Falls back to lang, then to "Sample".
langstringFree-form language hint (ruby, php, java, kotlin, …) — not limited
to the four generated languages. Used for the tab's internal key and as the
label fallback.
x-codeSamples is all-or-nothing per operation: as soon as an
operation carries one custom sample, the entire generated rail for that
operation is replaced, and api.codeSamples no longer applies to it. List
every language you want shown — tabs appear in array order. Other operations
keep their generated rails untouched.This is the escape hatch for hand-written, copy-paste-correct examples: real SDK calls, framework-specific snippets, or languages Markline can't generate.
The playground
Write endpoints get an inline request explorer: an editable
Authorization field, the request fields, a Send button, and the live
response with status + latency. It's the same engine on every surface.
api.playground.mode"full" | "inline" | "explorer" | "off"full — inline inputs + the request console + the API Explorer modal.
inline — inline inputs + the request console, no modal.
explorer — read-only docs + console + the API Explorer modal.
off — a static cURL panel, no interactivity.
api.playground.proxy"auto" | "always" | "never"How requests reach your API. auto (default) tries a direct browser fetch
and falls back to the built-in server proxy on CORS failure; always forces
the proxy; never is direct-only — required for static export.
/api/playground) only runs on a Node/Vercel
deployment and is dropped in static export. On a static host, set
proxy: "never" and enable CORS on your API, or the explorer can't reach it.Events & webhooks
If your API emits webhooks or async events, Markline documents them alongside the endpoints that trigger them. Each resource grows an Events tab listing the events it can deliver — name, summary, payload schema, and a sample — and the two are cross-linked: an emitting endpoint shows a Triggers chip, and the event shows an Emitted by back-link to that endpoint. The tab only appears on resources that actually have events; a spec with none looks exactly as before.
Where events come from
Markline reads events from four sources and normalizes them into one model, so you can use whichever your toolchain already emits:
| Source | Where it lives | Use it when |
|---|---|---|
webhooks | document root (OpenAPI 3.1) | your spec is 3.1 and uses the native webhooks object |
x-webhooks | document root (Redoc extension) | you're on 3.0 and want a Redoc-compatible root list |
callbacks | on an operation | the event is a standard OpenAPI callback tied to one request |
x-events | on a tag or an operation (Markline) | you want the lightest annotation, especially from codegen like NestJS |
For root webhooks / x-webhooks, each entry is a Path Item — Markline takes
its first method operation and reads summary, description, the operation's
tags, and the JSON request body as the payload. A root webhook only appears
on a resource if its operation carries a matching tags entry; untagged
webhooks are parsed but have no resource to attach to.
The x-events extension
x-events is the most direct way to declare events. It takes either a
map of name → definition, or an array of names (references to events
defined elsewhere — no inline payload):
"x-events": {
"project.created": {
"summary": "A new project was created.",
"description": "Emitted once the project is provisioned and ready.",
"payload": { "$ref": "#/components/schemas/Project" },
"guide": "/guides/webhooks#project-created"
}
}
"x-events": ["project.created", "project.deleted"] // name-only references
Each definition field is optional:
summarystringOne-line description, shown as the event's heading.
descriptionstringLonger prose shown under the summary.
payloadJSON Schema | $refThe delivered payload. Inline schema or a $ref into components.schemas —
resolved at render to produce the attribute table and the sample JSON.
(schema and a JSON requestBody are accepted as fallbacks.)
guidestringLink to a docs page or anchor that explains the event in depth, rendered as a
follow-on link. (guideHref / href are accepted too.)
Tag level vs. operation level changes how the event is wired:
- On an operation (
paths.<path>.<method>.x-events), the event inherits that operation'stags, so it groups onto those resources — and because an endpoint triggers it, the endpoint gets a Triggers chip and the event an Emitted by back-link. - On a tag (
tags[].x-events), the event attaches to that resource only, with no emitter back-link. Use this for events not tied to a single endpoint — delivered by an external processor, a batch job, or a state change.
Markline merges the two per resource and de-duplicates by event name, so an event declared on both a tag and an operation appears once.
api/openapi.json in this repo is wired up:
createProject emits project.created, createDeployment emits
deployment.succeeded / deployment.failed, and a root x-webhooks entry adds
project.archived to the Projects resource. Open the
Projects reference to see the Events tab.From NestJS
NestJS has no @ApiCallbacks, but @ApiExtension is a clean fit — and it's why
x-events exists. Annotate the handler that causes the event; Markline
aggregates it onto the resource automatically (no document-root wiring):
import { ApiExtension, ApiExtraModels, ApiTags, getSchemaPath } from '@nestjs/swagger';
@ApiTags('cards')
@ApiExtraModels(CardCreatedEvent) // emits the payload into components.schemas
@Controller('cards')
export class CardsController {
@Post()
@ApiExtension('x-events', {
'card.created': {
summary: 'A new card was issued',
payload: { $ref: getSchemaPath(CardCreatedEvent) },
guide: '/guides/webhooks#card-created',
},
})
create() { /* … */ }
}
@ApiExtraModels(CardCreatedEvent)makesgetSchemaPath(...)resolve — without it the$refpoints at a schema that was never emitted.- The event inherits
@ApiTags('cards'), so it lands on the Cards resource andPOST /cardsgets the Triggers chip. @ApiExtensionrequires anx--prefixed key and an object value — both hold. Placed on the controller class it applies to every route; on a handler, just that operation.
Reuse a shared set with a small composed decorator:
import { applyDecorators } from '@nestjs/common';
import { ApiExtension } from '@nestjs/swagger';
export const Emits = (events: Record<string, unknown>) =>
applyDecorators(ApiExtension('x-events', events));
// @Emits({ 'card.created': { summary: 'A new card was issued', payload: { $ref: getSchemaPath(CardCreatedEvent) } } })
x-events by post-processing the generated document before you write
openapi.json — NestJS has no decorator for tag objects.MDX overlays
Layer authored MDX on top of the generated reference — full components (callouts, tables, tabs) included:
api/
├── openapi.json
├── introduction.mdx # replaces the whole reference landing
├── sections/<tag>.mdx # rich intro for a resource (overrides the tag description)
└── operations/<operationId>.mdx # extra prose on one operation page
sections/<tag>.mdxrenders as the resource intro. The filename is the slugified tag —Payment Methods→payment-methods.mdx.operations/<operationId>.mdxrenders between the endpoint path and the auto-generated parameters.
AI & page actions
Every resource section carries the same affordances as the docs: Copy for LLM (copies the section as Markdown), View as Markdown, and — when Ask AI is enabled — Ask about this section, which opens the docked assistant with that section as context.
Versions
The reference header shows a version selector. With a single spec it surfaces
the spec's info.version (a semver 1.4.2 → v1 · 1.4.2; a date 2025-06-01
→ shown once, verbatim).
With multiple versions configured, each version ships its own
<id>/api/openapi.json, served at /api-reference/<id> — the selector switches
the spec in place and every link stays under that version's prefix. See
Versions & i18n for the folder layout.