Documenting an HTTP API
An HTTP API is the one documentation topic most projects already describe in a machine-readable
file. docspack build --openapi reads that file and writes one chunk per operation:
npx docspack build --openapi ./openapi.json --name @acme/docspack --pkg-version 1.4.0Combine it with --from to publish prose and an API in one package, which is what a library
with both usually wants:
npx docspack build --from ./docs --openapi ./openapi.jsonOr put both in the docspack key of the package’s package.json, so docspack build with no
arguments does the same thing:
{
"docspack": {
"from": "./docs",
"openapi": "./openapi.json",
"documents": "acme@1.4.0"
}
}What one chunk contains
Everything needed to make the call, and nothing else: the base URL, the credential, the inputs
with their types, the body shape, the response, the failures, the types they reference, and a
runnable curl line. The heading is the endpoint, the operation’s own prose comes first, and
the rest is one block:
[meta]
api: Invoice Service
base: https://api.example.com/v2
auth: bearer header:Authorization
[ops]
createInvoice POST /invoices
Creates a draft invoice.
> body: InvoiceCreate
< Invoice # 201 The created invoice.
@auth bearerAuth
[types]
InvoiceCreate:
customer_id: str
lines: [LineItem]
due_date?: date
[errors]
401 Error # The token is missing, expired or invalid.
422 Error # The line items did not validate.That notation is LAPIS, an open format for describing an API to a model rather than to a code generator. It is used here rather than JSON because the same facts cost far less. Measured against Stripe’s published document — 594 operations, 1,454 schemas — one operation’s chunk is around 830 tokens. The document it came from is two million.
A chunk carries what the operation reaches, bounded. Where a shape was too large to expand, the
chunk says so — +32 more, {...}, # not expanded here: — rather than presenting a partial
shape as a whole one.
Asking for one request
An endpoint is a key, not a phrase. Ranked as prose, POST /v1/charges matches every chunk that
mentions charges, and the overview page usually wins. So every operation chunk answers to both
spellings of its endpoint, looked up exactly:
docspack ask "POST /v1/charges"
docspack ask "createCharge"A concrete URL reaches the template the document declared, which is the useful direction: what
you have is an id out of a log line, and what the document contains is {charge}.
docspack ask "GET /v1/charges/ch_3OxTmP2eZvKYlo2C" # finds GET /v1/charges/{charge}Naming no method matches every method on that path:
docspack ask "what can I do with /v1/charges"The MCP tool answers the same way, because it is the same code path.
Chunk ids are the endpoint
An operation’s chunk id comes from its method and path — post-v1-charges — not from its
summary and not from its operationId. A summary gets reworded and an operationId gets
renamed; the endpoint changes only when the endpoint changes. That matters because a chunk id is
what docspack feedback add --chunk pins and what a published link resolves to.
JSON or YAML
Either, decided by what the file contains rather than by its extension — so a .yaml file holding
JSON works too. The reader is @docspack/lapis,
which is also a command of its own if all you want is the conversion:
npx @docspack/lapis ./openapi.yaml --statsWhat is not read
Swagger 2.0. Convert it first: npx swagger2openapi openapi.json -o openapi3.json.
A document split across files. An external $ref is refused rather than quietly dropped,
because a schema replaced by “unknown” is a chunk claiming an endpoint takes no body. Bundle
first: npx @redocly/cli bundle openapi.yaml -o openapi.json.
Each of these stops the build and names the command to run.
The page a person reads
The same document renders a reference — parameters, schemas, responses, code samples in cURL,
JavaScript, Python and Go, and a console that sends the request — through
@docspack/sheaf-react. docspack.dev does this with its own API:
the reference for a reader,
/api.lapis for an agent that needs the map, and
/api/<operationId>.md for an agent that already knows which endpoint it wants.
That Invoice Service is an actual document in this repository, and it renders as a page too:
docspack.dev/api/example — a bearer token, request bodies, a
DELETE and an error table, which this site’s own read-only API has none of. The chunk above is
cut down to fit; the page is the whole of it. Its document is
/api/example.json and its LAPIS is
/api/example.lapis: the whole API in about 900 tokens
against 5,700 for the JSON. Open the two beside each other — that pair is the entire idea.
The console never stores a credential. It is read from the field when you press Send and kept in nothing that outlives the request.
Endpoints are not exported names
docspack verify compares a chunk’s entities against the names a library exports. POST /v1/charges is not one, so an operation chunk is neither accused of drift nor cleared of it.
Proving an endpoint still exists means asking the server, which is a different tool from this
one.