Skip to main content

OpenAPI

The framework generates an OpenAPI 3.1 document from your controllers. Paths, parameters, request bodies, security, and tags are all derived from the route definitions you already write — point Swagger UI, Redoc, or a client-SDK generator at the output.

Generate

node src/cli.ts openapi # print to stdout
node src/cli.ts openapi --output openapi.json # write to a file

Add a script to package.json:

"openapi": "node src/cli.ts openapi"
npm run openapi -- --output openapi.json
note

The command loads your controllers but opens no database or network connection and binds no port — it walks the route registry and reads your schemas in-process. It's safe to run in CI. (Unlike codegen, which is on your hot path, this is a cold, occasional command, so it loads the real controller instances to read live schema objects.)

What gets documented

Everything comes from the route definitions you already have — there is nothing OpenAPI-specific to maintain separately:

OpenAPI fieldSource
operationIdhandler method name
tagscontroller class name
summaryroute description
path parameters:name path segments
query parametersroute query: schema (+ middleware query schemas)
requestBodyroute request: schema or content-type map (+ middleware request schemas)
securitymiddleware static get usedAuthParameters()
info / serversyour package.json + the http config (port, myDomain)

Output is OpenAPI 3.1 (JSON Schema 2020-12) only.

Request bodies come from your schemas

Body and query shapes are produced by introspecting the same validation schemas you already use at runtime — through the validator driver's toJsonSchema. How much detail you get depends on the validator:

ValidatorOpenAPI body
Zodfull JSON Schema via native z.toJSONSchema — types, formats, min/max, patterns, enums, defaults
YupJSON Schema from .describe() — types, required, enums, nullable, arrays, datedate-time
ArkType / any schema exposing a .toJsonSchema() methodits native output
defineSchemaoptional explicit jsonSchema; otherwise a placeholder schema + a warning
A hand-rolled ~standard functionnot introspectable unless it exposes .toJsonSchema() — a placeholder schema + a warning

Zod request-input semantics

OpenAPI describes what a client sends, not the value after Zod transforms it. For example, z.string().transform(Number) is documented as a string. z.coerce.date() is documented as a { "type": "string", "format": "date-time" } request value. Other Zod values that cannot be represented in JSON Schema, such as custom instanceof checks, safely degrade to {} rather than aborting the whole document.

For file uploads, use a content-type map with an explicit multipart/form-data entry. A custom runtime instanceof check cannot by itself tell OpenAPI that a field contains binary data.

:::note Describe imperative schemas explicitly defineSchema callbacks are imperative, so their code cannot be inferred as a shape. Pass its optional jsonSchema option when the endpoint should be documented, or use a declarative validator such as Zod or Yup. A custom ~standard schema can likewise expose a .toJsonSchema() method. Without either, the generator emits a placeholder object and prints a warning, e.g.:

OpenAPI: 1 schema(s) could not be fully introspected:
POST /auth/login body: schema introspection unavailable.

:::

Schema conversion is contained per route and middleware schema. Warnings name the HTTP method, route, and schema position. An unavailable body schema gets a placeholder object, an unavailable query schema is omitted, and other healthy routes remain fully documented. Genuine command boot, generator, and file-write failures still exit nonzero.

The built-in Pagination middleware already supplies its explicit schema, so every route using it documents optional numeric page and limit query parameters without application-specific OpenAPI annotations.

This is why the generator must load your controllers at runtime rather than read the generated types: JSON Schema can only be produced from the live schema object (z.toJSONSchema(...), schema.describe()), not from a TypeScript type.

Documenting auth (security schemes)

A middleware advertises the security scheme(s) it enforces with a static get usedAuthParameters() getter. The generator reads it off the class — no instantiation — adds each entry to components.securitySchemes, and attaches a security requirement to every operation whose middleware chain includes that middleware.

import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";

class TokenAuth extends AbstractMiddleware {
static get usedAuthParameters() {
return [
// http bearer scheme
{ name: "bearerAuth", type: "http", scheme: "bearer", description: "Bearer token" },
// or an apiKey header
{ name: "X-Api-Key", type: "apiKey", in: "header", description: "API key" },
];
}

async middleware(req, res, next) {
// ... runtime auth logic ...
}
}
FieldMeaning
namescheme key in components.securitySchemes (for apiKey, also the header/query name)
type'apiKey' or 'http'
infor apiKey: 'header' (default) / 'query' / 'cookie'
schemefor http: 'bearer', 'basic', …
descriptionshown in the docs UI

The built-in GetUserByToken already declares its Authorization header + bearer schemes, so any route behind it is documented as secured automatically.

Current limitations

  • Response bodies are not yet schema-documented. Every operation carries a generic 200/400/401/404 response with a text description but no body schema. (Documenting response shapes is on the roadmap — it needs a declared response: schema, since a response has no runtime schema object to introspect.)
  • Catch-all ({*splat}) routes are approximated as a single {splat} path parameter, because OpenAPI has no catch-all; the command warns when it does this.
  • The route description becomes the operation summary; there is no separate long description, deprecated flag, or per-tag description yet.