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; typed by the route params: schema when declared, otherwise string
query parametersroute query: schema (+ middleware query schemas)
requestBodyroute request: schema or content-type map (+ middleware request schemas)
securitymiddleware static get authSchemes() (schemes) and static get requiresAuth() (required vs optional)
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, query and path-param 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, date → date-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 reads credentials from with a static get authSchemes() getter. The generator reads it off the class — no instantiation — adds each entry to components.securitySchemes, and attaches a security entry 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 authSchemes() {
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

:::note Renamed in 5.5.0 authSchemes was called usedAuthParameters before. The old name still works until v6, with a one-time deprecation warning; rename it in your middleware. :::

Required or optional​

Reading a token is not the same as requiring one. A middleware that rejects requests without an authenticated user says so with static get requiresAuth():

class TokenAuth extends AbstractMiddleware {
static get requiresAuth() {
return true; // this middleware answers 401 without a user
}
}
  • If any middleware in a route's chain has requiresAuth, the operation's security is required: security: [{ bearerAuth: [] }, …].
  • If the chain only reads credentials, the security is optional: security: [{}, { bearerAuth: [] }, …]. The empty {} tells clients an anonymous request is valid too, so generated clients send a token when they have one but don't require it.

The built-in GetUserByToken declares the Authorization header and bearer schemes and only reads the token, so on its own it documents auth as optional (for example, a public login route). The built-in Auth and Role middleware have requiresAuth, so routes behind them are documented as requiring a token.

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.