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
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 field | Source |
|---|---|
operationId | handler method name |
tags | controller class name |
summary | route description |
path parameters | :name path segments |
query parameters | route query: schema (+ middleware query schemas) |
requestBody | route request: schema or content-type map (+ middleware request schemas) |
security | middleware static get usedAuthParameters() |
info / servers | your 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:
| Validator | OpenAPI body |
|---|---|
| Zod | full JSON Schema via native z.toJSONSchema — types, formats, min/max, patterns, enums, defaults |
| Yup | JSON Schema from .describe() — types, required, enums, nullable, arrays, date → date-time |
ArkType / any schema exposing a .toJsonSchema() method | its native output |
defineSchema | optional explicit jsonSchema; otherwise a placeholder schema + a warning |
A hand-rolled ~standard function | not 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 ...
}
}
| Field | Meaning |
|---|---|
name | scheme key in components.securitySchemes (for apiKey, also the header/query name) |
type | 'apiKey' or 'http' |
in | for apiKey: 'header' (default) / 'query' / 'cookie' |
scheme | for http: 'bearer', 'basic', … |
description | shown 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/404response with a text description but no body schema. (Documenting response shapes is on the roadmap — it needs a declaredresponse: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
descriptionbecomes the operationsummary; there is no separate long description,deprecatedflag, or per-tag description yet.