Skip to content

AdvancedOpenAPI Export

OpenAPI Export

Write an API's definition as an OpenAPI 3.1 or 3.2 document — for a documentation portal, an API gateway, another team's SDK generator or a contract test.

3 min readboth modes

One definition, one document

The definition your client is typed from also describes the API to everyone else. toOpenAPI writes it as an OpenAPI document — for a documentation portal, an API gateway, another team's SDK generator, a contract test — and Courier's own import reads such a document back, so an API can go from a specification to code and back again.

scripts/openapi.ts
import { writeFile } from "node:fs/promises"
import { toOpenAPI } from "@noverax/courier/openapi"

import { billing } from "../api/billing"

const document = toOpenAPI(billing, { info: { title: "Billing", version: "2.4.0" } })
await writeFile("openapi.json", JSON.stringify(document, null, 2))

Studio's command writes the sources courier.config.ts declares, without a script:

bash
npx @noverax/courier-studio openapi --source billing --api-version 2.4.0 --out openapi.json

With no --source, every source is written to the folder --out names, one <source>.openapi.json each. With one source and no --out, the document is printed, ready for a pipe.

Studio itself writes the same document from its APIs page: Export OpenAPI picks a source, the OpenAPI version, the document's title and the API's version, shows the document and every endpoint it leaves out, and downloads it as <source>.openapi.json or copies it. It is written from the definition Studio runs, so it follows the code as you change it.

What becomes what

In the definitionIn the document
An endpointAn operation, whose operationId is its call in the client: billing.invoices.list
A groupA tag; a nested group's tag names its parent in OpenAPI 3.2
A path parameter, :idA required path parameter, {id}
Variables of a GET, HEAD, OPTIONS or DELETEQuery parameters, required as the schema says
Variables of the other verbsThe request body: JSON, or url-encoded or multipart where the endpoint's closure sends formBody or FormData
A query written into the path, models?beta=trueA required query parameter with its one value
An absolute pathThe operation's own server
The answer's schemaThe 200 response
A streamA text/event-stream response: its events as itemSchema in 3.2, as x-courier-event-schema in 3.1
sessionSecurity schemes: each cookie it is kept in, its bearer cookie as a bearer token, its double-submit header required on writes; an endpoint with session: false requires none
.headers() and idempotencyKeyHeader parameters
.meta({ summary, description })The operation's summary and description
deprecateddeprecated: true, the reason in the description

Schemas

Schemas are written as JSON Schema 2020-12, the dialect OpenAPI 3.1 and 3.2 take, through Standard JSON Schema: Zod 4 and ArkType describe their own schemas. A library without it is described by a converter you pass — Valibot's, for one:

ts
import { toJsonSchema } from "@valibot/to-json-schema"

toOpenAPI(api, { jsonSchema: (schema) => toJsonSchema(schema as never) })

An endpoint declared by its types alone, .response<User>(), has no schema at run time, and its input and answer are left unspecified. A value JSON cannot hold — a Date field, say — is left unspecified rather than failing its whole schema. The definitions a recursive schema carries move to the document's components, its references with them.

What is left out

Three kinds of endpoint have no operation, and onSkip hears each one and why — the command says them in its log, and Studio's export lists them beside the document:

  • A channel: OpenAPI has no form for a WebSocket.
  • An endpoint the application serves itself with .implement(): it has no address of its own.
  • A QUERY endpoint in OpenAPI 3.1, which has no place for the method — write openapi: "3.2.0".

Options

OptionWhat it does
openapi"3.1.0", the default, or "3.2.0" for QUERY, a stream's event schema and nested tags
infoThe document's title, version and description; the title defaults to the API's name, the version to 0.0.0
serversWhere the API is served; its absolute baseURL by default
jsonSchemaA converter for schemas whose library has no Standard JSON Schema
onSkipTold of each endpoint the document leaves out, and why

Every document the export writes is checked in Courier's tests against the OpenAPI Initiative's own schema for its version, and read back by Courier's import.