Docs / Packages / @zudojs/openapi
v1.5.0

@zudojs/openapi

Turns your routes and schemas into an OpenAPI 3.0 or 3.1 document, checks that the document is valid, and serves it — together with a ready-made documentation page.

OPENAPI SPECIFICATION VALIDATION SWAGGER UI REDOC

OVERVIEW

An OpenAPI document is a machine-readable description of an HTTP API. It lists every path, every method, what you send, and what comes back. It is normally written as one JSON or YAML file.

Because it is machine-readable, tools can read it and do work for you: render browsable documentation, generate a client library in another language, produce request collections, or check in CI that your API has not changed by accident.

Writing that file by hand goes stale the moment code changes. @zudojs/openapi builds it from the route metadata and schemas you already have, so the description and the code move together.

USE IT WHEN
  • You expose an HTTP API and want documentation that cannot drift from the code.
  • Someone needs a generated client or SDK for your API.
  • You want a contract check in CI that fails when the spec becomes invalid.
  • You want a Swagger UI or ReDoc page without wiring one up yourself.
SKIP IT WHEN
  • Your service is internal-only and nobody reads a spec for it.
  • You only need runtime input validation — that is @zudojs/schema.
  • Your API is GraphQL or RPC — OpenAPI describes HTTP endpoints.
In plain words: this package writes the openapi.json file for you, tells you when that file is wrong, and hands you a web page that displays it.

INSTALLATION

$ npm install @zudojs/openapi

The runtime dependencies are @zudojs/errors (v1.3.0) and @zudojs/constants (v1.1.2), which supplies the schema ceilings the converter emits. Install @zudojs/schema too if you want to register schemas rather than hand-written OpenAPI objects:

$ npm install @zudojs/schema

Schemas are read structurally, not imported, so any object with the same runtime shape works.

These docs follow the framework source. If an export shown here is missing from the version you installed, update to the latest @zudojs release.

QUICK START

OpenAPIManager is the one class most applications use. You give it document metadata, add routes, then ask for the document.

import { OpenAPIManager } from "@zudojs/openapi"; const manager = new OpenAPIManager({ version: "3.1.0", info: { title: "Orders API", version: "1.0.0" }, servers: [{ url: "https://api.example.com" }], }); manager.addRoute({ method: "get", path: "/orders/:id", metadata: { openapi: { operationId: "orders.get", summary: "Get one order", parameters: [{ name: "id", in: "path", schema: { type: "string" } }], responses: { "200": { description: "The order" }, "404": { description: "No such order" }, }, }, }, }); const document = manager.generate(true); // true = validate while generating console.log(Object.keys(document.paths)); // [ '/orders/{id}' ] console.log(JSON.parse(manager.toJSON()).openapi); // 3.1.0

Three things happened without you asking:

  • /orders/:id became the OpenAPI path template /orders/{id}.
  • The id parameter was marked required: true, because OpenAPI requires that of every path parameter.
  • The document got a info["x-logo"] entry so viewers show a logo. See Branding.
Tip: generate() is safe to call as many times as you like. Every change to the manager throws away the cached document, so the next call sees your new routes.

DESCRIBING ROUTES

A route here is a method, a path, and some metadata. The metadata lives under metadata.openapi and holds the fields OpenAPI calls an operation: what this endpoint is called, what it takes, and what it returns.

Every field is optional. The main ones:

FieldWhat it does
operationIdUnique name for the endpoint. Code generators turn it into a method name.
summary / descriptionShort and long human text.
tagsGroups endpoints together in the rendered page.
parametersPath, query, header or cookie inputs. Each has name, in, and optionally schema, required, example.
params / query / headers / cookiesShorthand: one object schema each, and every property becomes a parameter.
requestBodyThe body the endpoint accepts, keyed by media type.
bodyShorthand: a schema sent as application/json, or { schema, contentType?, required? }.
responsesKeyed by status code, a 4XX-style range, or default. Each is a Response Object with a description, or the shorthand { schema }, whose description defaults to the reason phrase. Leave it out and the operation gets default: "Undocumented response" plus a warning (see below).
security / servers / externalDocsPer-operation overrides of the document-level values. security: [] marks the operation public.
deprecatedMarks the endpoint as going away.
hiddenLeaves the route out of the generated document entirely.

This adds a second route with a body and a tag, then prints the methods that ended up on the path:

import { OpenAPIManager } from "@zudojs/openapi"; const manager = new OpenAPIManager({ info: { title: "Orders API", version: "1.0.0" }, }); manager.addRoute({ method: "post", path: "/orders", metadata: { openapi: { operationId: "orders.create", tags: ["orders"], requestBody: { required: true, content: { "application/json": { schema: { type: "object" } }, }, }, responses: { "201": { description: "Order created" } }, }, }, }); const document = manager.generate(); console.log(Object.keys(document.paths["/orders"] ?? {})); // [ 'post' ]

Use addRoute for a route you are adding once; it throws if the same route is already registered. Use setRoute when replacing is what you want, and removeRoute(method, path) to drop one.

Since v1.4.0 “the same route” means the same method and the same OpenAPI path template, not the same source string. GET /users/:id and GET /users/{id} both become the path item /users/{id}, so the second one is now rejected with an OpenAPIRouteError. Before v1.4.0 both registered and generation silently kept only the last one: an operation disappeared from the published document and validate() reported nothing wrong. hasRoute, setRoute and removeRoute accept either spelling for the same route.

Watch out: OpenAPI has no optional or wildcard path segments. /files/* and /users/:id? both throw an OpenAPIRouteError instead of quietly producing a path template no tool understands. Generating from an @zudojs/http router avoids this: the router resolves those patterns first (an optional segment becomes two paths, a wildcard a {rest} slot).

Routes with no documented responses

OpenAPI requires every operation to list at least one response. If a route declares none (no responses field, or responses: {}), the generator does not make one up. It emits a default response described as "Undocumented response", which keeps the document valid while saying honestly that nothing is known, and it reports a warning so you can fix the route:

import { createOpenAPIDocumentFromRoutes } from "@zudojs/openapi"; const options = { info: { title: "Users API", version: "1.0.0" }, onRouteWarning: (message) => console.warn("warning:", message), }; // No responses declared. const before = createOpenAPIDocumentFromRoutes([{ method: "DELETE", path: "/users/:id" }], options); console.log(before.paths["/users/{id}"].delete.responses); // The response the endpoint really sends. const after = createOpenAPIDocumentFromRoutes( [{ method: "DELETE", path: "/users/:id", responses: { "204": { description: "User deleted" } } }], options, ); console.log(after.paths["/users/{id}"].delete.responses);
$ node responses.mjs warning: DELETE /users/:id: no responses are documented; emitted "default: Undocumented response". { default: { description: 'Undocumented response' } } { '204': { description: 'User deleted' } }

Before v1.5.0 such a route got an invented "200": { description: "OK" }. That was a guess, and often a wrong one: a DELETE that answers 204 No Content was published as returning 200, so a client generated from the document expected a body that never came. The guess also hid the problem, because the validator's “every operation declares a response” check could never fire.

The warning reaches three places: the onRouteWarning(message) option (accepted by new OpenAPIManager, createOpenAPIDocumentFromRoutes and createOpenAPIManagerFromRoutes), onSchemaWarning under the name "routes", and manager.routeWarnings(). @zudojs/http's generateOpenAPIDocument passes its own onRouteWarning through, so these arrive next to its duplicate-route warnings.

Watch out: regenerate any checked-in spec after upgrading to v1.5.0. Every operation that relied on the invented 200 now shows default instead, and each one logs a warning. The fix is to declare the responses the endpoint really returns; the warning then goes away.

GENERATING FROM A ROUTE TABLE

Most of the time you do not want to call addRoute once per endpoint. Your routes already exist somewhere — in a router, or in a list of operations — and the document should come from that list, so it can never describe an endpoint that is not there.

createOpenAPIDocumentFromRoutes(routes, { info }) does that. routes is a plain array of route descriptors: one object per endpoint, holding its method, its path, and the same documentation fields as metadata.openapi, all flattened into one object. Schemas can be @zudojs/schema schemas, which are converted for you.

import { createOpenAPIDocumentFromRoutes } from "@zudojs/openapi"; import { objectSchema, stringSchema } from "@zudojs/schema"; const user = objectSchema({ id: stringSchema().uuid(), name: stringSchema() }); const document = createOpenAPIDocumentFromRoutes( [ { method: "GET", path: "/users/:id", summary: "Get a user", tags: ["users"], responses: { "200": { schema: user }, "404": { description: "No such user" } }, }, { method: "POST", path: "/users", operationId: "users.create", body: objectSchema({ name: stringSchema() }), responses: { "201": { schema: user } }, }, { method: "GET", path: "/health", security: [], // public responses: { "200": { description: "The service is up" } }, }, ], { info: { title: "Users API", version: "1.0.0" }, securitySchemes: { bearer: { type: "http", scheme: "bearer" } }, security: [{ bearer: [] }], // every route needs a bearer token... validate: true, }, ); console.log(Object.keys(document.paths)); // [ '/users/{id}', '/users', '/health' ]

What happened: /users/:id became /users/{id} with a required id parameter, the body schema became a required application/json request body, the 404 kept its description, and the 200 and 201 got theirs from the reason phrase. validate: true throws an OpenAPIValidationError if the result is invalid.

In plain words: security: [] on a route means "this one is public". It overrides the document-wide security, so /health needs no token while every other route does. Leaving security out means "use the document's setting".

The route descriptor

An OpenAPIRouteDescriptor is RouteOpenAPIMetadata plus method and path. Only those two are required.

FieldWhat it does
methodAny case. It must be one an OpenAPI path item can hold: get put post delete options head patch trace.
path/users/:id or /users/{id}. Optional, regex-constrained and wildcard segments have no OpenAPI spelling; the route source resolves them before handing the path over.
summary, description, operationId, tags, deprecated, servers, externalDocsCopied to the operation.
securityOperation security. [] marks it public and overrides the document's security.
params, query, headers, cookiesOne object schema each; every property becomes a parameter. required comes from the schema, and path parameters are always required.
bodyA schema (sent as application/json, required), or { schema, contentType?, required?, description?, example? }. A raw requestBody wins over it.
responsesKeyed by status, NXX range or default: a Response Object, or { schema, description?, contentType?, headers?, example? }. The description defaults to the reason phrase, e.g. "Not Found".
parametersExplicit parameters. Highest precedence.
inferredParametersParameters the source worked out itself, such as a regex constraint. Lowest precedence.
hiddentrue leaves the operation out.

Every path template slot is documented as a required string parameter even when nothing declares it. A declared path parameter the template does not contain is dropped with a warning, rather than producing an invalid document. Warnings reach onRouteWarning, onSchemaWarning under the name "routes", and manager.routeWarnings(). A route with no responses warns too (see Routes with no documented responses).

Where descriptors come from

  • @zudojs/http builds them from a router's registered routes: generateOpenAPIDocument(router, { info }) and mountOpenAPI(router, { info }) call this function for you.
  • @zudojs/api builds them from operations: toOpenAPIRouteDescriptors(registry, { basePath }).
  • Or write the array yourself, as above.

Keeping a manager instead

createOpenAPIManagerFromRoutes(routes, options) takes the same arguments but returns the OpenAPIManager, so you can serve it with toResponse() and toUIResponse(). When your routes change, manager.setRoutes(routes.map(routeDescriptorToRouteInfo)) replaces the whole route set at once, rejecting duplicates before anything changes. routeDescriptorToRouteInfo converts one descriptor into the { method, path, metadata } shape addRoute takes.

SCHEMAS

A schema describes the shape of a value: which fields exist, their types, and what counts as valid. OpenAPI has its own schema dialect, and addSchema translates a @zudojs/schema schema into it.

Registering a schema puts it in components.schemas under the name you give, so operations can point at it with a $ref instead of repeating it.

import { OpenAPIManager } from "@zudojs/openapi"; import { objectSchema, stringSchema, numberSchema } from "@zudojs/schema"; const manager = new OpenAPIManager({ info: { title: "Orders API", version: "1.0.0" }, }); manager.addSchema( "Order", objectSchema({ id: stringSchema().uuid(), total: numberSchema().min(0), }), ); const document = manager.generate(); console.log(JSON.stringify(document.components?.schemas?.Order)); // {"type":"object","properties":{"id":{"type":"string","format":"uuid","maxLength":255}, // "total":{"type":"number","minimum":0}},"required":["id","total"]}

optional, default, any and unknown fields are left out of required, matching what the runtime accepts; constraints on coerced schemas and factory defaults are carried into the document. A string or array with no explicit maximum gets the limit the runtime enforces (maxLength: 255 / maxItems: 1000), read from @zudojs/constants SCHEMA_DEFAULT_MAX_STRING_LENGTH / SCHEMA_DEFAULT_MAX_ARRAY_LENGTH.

Point an operation at it with createComponentReference, which builds the $ref string and escapes names containing / or ~:

import { createComponentReference } from "@zudojs/openapi"; console.log(createComponentReference("schemas", "Order")); // { $ref: '#/components/schemas/Order' }

Unknown keys

An object schema decides what happens to a key it does not declare: .strip() (the default) discards it and accepts the payload, .passthrough() keeps it, and .strict() rejects the payload. Only .strict() emits additionalProperties: false, because that keyword is OpenAPI for “reject the payload”.

import { convertSchema } from "@zudojs/openapi"; import { objectSchema, stringSchema } from "@zudojs/schema"; const shape = () => objectSchema({ id: stringSchema() }); console.log(convertSchema(shape()).schema.additionalProperties); // undefined — a stripping object accepts the extra key console.log(convertSchema(shape().strip()).schema.additionalProperties); // undefined — same contract, so the same document console.log(convertSchema(shape().strict()).schema.additionalProperties); // false

Before v1.4.0 a stripping object emitted additionalProperties: false too. A client generated from that document refused requests the service would have accepted — it declared the extra key fatal while the parser was quietly dropping it. It also made objectSchema({…}) and objectSchema({…}).strip() document differently despite validating identically. Both now produce the same schema.

Watch out: regenerate any checked-in spec after upgrading to v1.4.0. Every object that is not .strict() loses its additionalProperties: false, so the first regeneration produces a diff that is expected rather than a regression. A spec file that is never regenerated keeps publishing the old, stricter contract.

When a constraint cannot be expressed

Some things your schema can say have no OpenAPI equivalent. Rather than emit an empty {} and let you find out in production, the converter records a warning and still emits everything it can.

import { convertSchema } from "@zudojs/openapi"; import { stringSchema } from "@zudojs/schema"; const { schema, warnings } = convertSchema( stringSchema().regex(/^abc$/i), ); console.log(schema.pattern); // '^abc$' console.log(warnings.length); // 1 — the /i flag cannot be expressed

OpenAPI's pattern keyword carries the regular expression source and nothing else — there is no place to put i or m. A case-insensitive pattern would therefore become case-sensitive in the published document, which is stricter than the code that actually validates requests. The converter no longer lets that pass unremarked: the flags show up in a warning naming the pattern.

Warnings from a schema you registered are collected per component:

const manager = new OpenAPIManager({ info: { title: "Orders API", version: "1.0.0" }, onSchemaWarning: (name, warnings) => console.warn(name, warnings), }); manager.schemaWarnings(); // Map<componentName, warnings>

Already have an OpenAPI schema object, hand-written or from somewhere else? Register it as-is with addRawSchema(name, schema), which skips conversion.

3.0 VS 3.1

OpenAPI 3.0 and 3.1 spell several schema keywords differently. Emitting the 3.1 spelling into a 3.0 document does not fail loudly — a strict tool rejects the whole file and a lenient one drops the keyword, so the constraint is simply gone.

The converter takes the target version from the manager and emits the spelling that version defines. You choose the version once, in the constructor.

Your constraint3.1.x3.0.x
gt(5)exclusiveMinimum: 5minimum: 5, exclusiveMinimum: true
lt(10)exclusiveMaximum: 10maximum: 10, exclusiveMaximum: true
nullable valuetype: ["string", "null"]nullable: true
literalconst: "yes"enum: ["yes"]
tupleprefixItemsminItems / maxItems

In 3.1, exclusiveMinimum holds the number. In 3.0 it is a boolean that modifies minimum. Both spellings now come out right:

import { convertSchema } from "@zudojs/openapi"; import { numberSchema } from "@zudojs/schema"; const price = numberSchema().gt(5); console.log(convertSchema(price, { version: "3.1.0" }).schema); // { type: 'number', exclusiveMinimum: 5 } console.log(convertSchema(price, { version: "3.0.3" }).schema); // { type: 'number', minimum: 5, exclusiveMinimum: true }
In plain words: pick "3.1.0" unless a tool you depend on only reads 3.0. Either way the constraints you wrote survive the translation.

VALIDATION

A document can be well-formed JSON and still be a broken OpenAPI file. The validator reads a finished document and reports what is wrong before a tool downstream trips over it.

validate() reports; it never throws:

import { OpenAPIManager } from "@zudojs/openapi"; const manager = new OpenAPIManager({ info: { title: "Orders API", version: "1.0.0" }, }); manager.addRoute({ method: "get", path: "/orders/:id", metadata: { openapi: { operationId: "orders.get", responses: { "200": { description: "The order" } } } }, }); manager.addRoute({ method: "get", path: "/orders", metadata: { openapi: { operationId: "orders.get", responses: { "200": { description: "All orders" } } } }, // copy-paste slip }); const result = manager.validate(); console.log(result.valid); // false console.log(result.errors[0].message); // 'Duplicate operationId "orders.get" (also used by GET /orders/{id}).'

Two operations share the operationId "orders.get", so a code generator would produce two methods with the same name. Rename the second one (say, "orders.list") and it passes.

What it checks

  • Required document fields are present and openapi names a supported version.
  • Every operation declares at least one response, keyed by a status code, a 4XX-style range or default, each with a description. Generated documents always pass this one (an undocumented route gets default and a warning), so it matters most for documents built by hand.
  • Path templates and in: "path" parameters agree in both directions, path parameters are required, no parameter is declared twice in one list (an operation-level parameter may override a path-level one), and no two paths are identical apart from their template parameter names.
  • operationId values are unique and within MAX_OPERATION_ID_LENGTH.
  • Every security requirement names a scheme declared in components.securitySchemes, and every declared scheme is used somewhere.
  • Every local $ref resolves inside the document, and every non-local one uses an allowed scheme.
  • No path still uses :id instead of {id}.

Where a $ref may point

A $ref is an instruction to whatever reads the document: go fetch this and paste it here. Most refs are local — #/components/schemas/Order — and the validator checks they resolve.

A ref that is not local is checked for its URI scheme. Only http and https are allowed. Anything else is an error.

import { createOpenAPIValidator } from "@zudojs/openapi"; const validator = createOpenAPIValidator(); const result = validator.validate({ openapi: "3.1.0", info: { title: "Orders API", version: "1.0.0" }, paths: {}, components: { schemas: { Leak: { $ref: "file:///etc/passwd" } } }, }); console.log(result.valid); // false console.log(result.errors[0].message); // mentions the "file:" scheme
Danger: before this check, any ref that did not start with # was accepted. A document carrying file:///etc/passwd or http://169.254.169.254/latest/meta-data/ validated cleanly, and the resolver that later followed it turned your spec into a file read or a request to a cloud metadata endpoint. Non-fetchable schemes are now rejected outright.

An https or relative ref is legal OpenAPI, so it is a warning, not an error: the document stays valid, but you are told something outside it will be fetched. Bundle the target into components if the source is not fully trusted.

Failing loudly

Pass true to generate, toJSON or toYAML and an invalid document throws instead. The error carries the issues:

import { OpenAPIValidationError } from "@zudojs/openapi"; try { manager.generate(true); } catch (error) { if (error instanceof OpenAPIValidationError) { console.error(error.format()); // one line per issue console.error(error.issues); // structured: { path, message, severity } } }
Tip: run manager.generate(true) in a CI test. The build fails the moment a route stops matching its documented contract.

SERVING THE DOCS

Two endpoints are all you need. One serves the specification, the other serves a page that reads it.

  • toResponse() — the document itself, as JSON or YAML.
  • toUIResponse({ specUrl }) — a complete HTML documentation page that fetches the spec from specUrl.

Both return the same plain shape — { status, headers, body } — so any HTTP adapter can turn them into its own response type.

import { OpenAPIManager } from "@zudojs/openapi"; const manager = new OpenAPIManager({ info: { title: "Orders API", version: "1.0.0" }, }); const spec = manager.toResponse({ format: "json" }); console.log(spec.status, spec.headers["content-type"]); // 200 application/json; charset=utf-8 const page = manager.toUIResponse({ specUrl: "/openapi.json" }); console.log(page.status, page.headers["content-type"]); // 200 text/html; charset=utf-8 console.log(page.body.startsWith("<!doctype html>")); // true

Wired into an HTTP framework, that is two handlers:

// pseudo-code for whatever router you use app.get("/openapi.json", () => manager.toResponse()); app.get("/docs", () => manager.toUIResponse({ specUrl: "/openapi.json" }));

On @zudojs/http you do not write these two handlers: mountOpenAPI(router, { info }) registers both, generated from the router's own routes.

Open /docs and you get a browsable page: every endpoint listed by tag, expandable request and response shapes, and a "try it out" button that sends a real request.

Swagger UI or ReDoc

Swagger UI is the default: interactive, good for poking at an API by hand. ReDoc renders a three-column reference document — better for reading, no try-it-out. Choose with renderer.

manager.toUIResponse({ specUrl: "/openapi.json", renderer: "redoc" });

You can also render the page on its own, without a manager, with renderOpenAPIUI. It returns the HTML string; serve it with content-type: text/html.

import { renderOpenAPIUI } from "@zudojs/openapi"; const html = renderOpenAPIUI({ specUrl: "/openapi.json", title: "Orders API", renderer: "swagger", }); console.log(html.includes("swagger-ui-bundle.js")); // true
OptionWhat it doesDefault
specUrlWhere the page fetches the document from. Required.
titlePage title and header text."API reference"
renderer"swagger" or "redoc"."swagger"
logoHeader logo, or false for none.Zudo wordmark
faviconFavicon URL or data URI, or false.Zudo favicon
customCssCSS appended after the built-in theme.
assetsBaseUrlWhere the viewer's own JS and CSS load from.pinned jsDelivr (swagger-ui-dist@5.33.0, redoc@2.5.4) with SRI
assetIntegritySRI hashes for the viewer assets, or false to omit integrity.the pinned hashes when assetsBaseUrl is unset; none when it is set
contentSecurityPolicycontent-security-policy header sent by toUIResponse, or false for none. Ignored by renderOpenAPIUI.restrictive policy from buildOpenAPIUIContentSecurityPolicy
connectSourcesExtra origins "Try it out" may call, added to connect-src. The document's absolute servers are added automatically.[]
swaggerOptionsExtra options passed to SwaggerUIBundle. Ignored by ReDoc.

Air-gapped deployments

By default the page loads exact, pinned versions of Swagger UI or ReDoc from cdn.jsdelivr.net with Subresource Integrity, and toUIResponse sends a restrictive content-security-policy header (pass contentSecurityPolicy: false to omit it). A machine with no internet access renders a blank page. Host the viewer's files yourself and point assetsBaseUrl at them:

manager.toUIResponse({ specUrl: "/openapi.json", assetsBaseUrl: "/vendor/swagger", }); // the page now loads /vendor/swagger/swagger-ui.css // and /vendor/swagger/swagger-ui-bundle.js

For ReDoc the file needed under that base is redoc.standalone.js.

Watch out: renderOpenAPIUI refuses input that would break out of the page. A javascript: or vbscript: URL throws a TypeError (the scheme is read after stripping control characters, so java\nscript: is caught), a data: URL that is not an image throws, an empty specUrl throws, and customCss containing </style> throws rather than being mangled — that sequence would end the style block and let the rest be parsed as HTML.

BRANDING

ReDoc, Scalar and several other viewers look for a logo in a non-standard info["x-logo"] field. Generated documents carry one by default, so a spec opened in any of those tools shows a logo instead of nothing.

import { OpenAPIManager } from "@zudojs/openapi"; const document = new OpenAPIManager({ info: { title: "Orders API", version: "1.0.0" }, }).generate(); console.log(document.info["x-logo"].altText); // 'Zudo' console.log(document.info["x-logo"].href); // 'https://zudojs.oyinlola.site'

The branding option controls it. There are three ways to use it:

ValueResult
omitted or trueThe Zudo mark, in x-logo and in the UI page header.
falseNo x-logo at all, and no logo on the page.
{ url, href, altText }Your own logo, used in both places.
const ownBrand = new OpenAPIManager({ info: { title: "Orders API", version: "1.0.0" }, branding: { url: "https://acme.example/logo.svg", href: "https://acme.example", altText: "Acme", }, }); console.log(ownBrand.generate().info["x-logo"].altText); // 'Acme' const plain = new OpenAPIManager({ info: { title: "Orders API", version: "1.0.0" }, branding: false, }); console.log(plain.generate().info["x-logo"]); // undefined

A logo you put on info yourself is never overwritten — set info["x-logo"] directly and that is what the document carries, whatever branding says.

The brand assets are exported too, as inline SVG strings and as data URIs, so a page can use them with no extra network request:

import { zudoLogo, svgToDataUri, ZUDO_MARK_SVG, ZUDO_SITE_URL, } from "@zudojs/openapi"; console.log(zudoLogo().href); // 'https://zudojs.oyinlola.site' console.log(ZUDO_SITE_URL); // 'https://zudojs.oyinlola.site' const uri = svgToDataUri(ZUDO_MARK_SVG); console.log(uri.startsWith("data:image/svg+xml;charset=utf-8,")); // true

The full set: ZUDO_MARK_SVG, ZUDO_MARK_DARK_SVG, ZUDO_WORDMARK_SVG, ZUDO_WORDMARK_DARK_SVG, ZUDO_FAVICON_SVG, and a _DATA_URI counterpart for each.

BUILDING BY HAND

If you are not generating from routes — describing an API you did not write, or assembling a document in a script — use OpenAPIDocumentBuilder. Every method returns the builder, so calls chain, and build() returns the finished document.

import { OpenAPIDocumentBuilder, toOpenAPIYAML } from "@zudojs/openapi"; const document = new OpenAPIDocumentBuilder({ info: { title: "Orders API", version: "1.0.0" }, }) .addServer({ url: "https://api.example.com" }) .addTag({ name: "orders" }) .addSecurityScheme("bearerAuth", { type: "http", scheme: "bearer" }) .addSecurity({ bearerAuth: [] }) .addSchema("Order", { type: "object" }) .addPath("/orders", { get: { responses: { "200": { description: "OK" } } }, post: { responses: { "201": { description: "Created" } } }, }) .build(); console.log(toOpenAPIYAML(document).split("\n")[0]); // openapi: 3.1.0

The builder and the manager assemble documents through the same registry, so both produce the same shape and obey the same rules.

Serialize any document with toOpenAPIJSON or toOpenAPIYAML. The YAML is real YAML: strings that YAML would otherwise reinterpret — true, null, 1.0, anything starting with a reserved character — come out quoted.

API REFERENCE

Classes

NameWhat it doesNotes
OpenAPIManagerCollects routes and schemas, generates, validates, serializes and serves.The class most apps use. createOpenAPIManager(options) is the factory.
OpenAPIDocumentBuilderChainable builder for a document written by hand.createOpenAPIDocumentBuilder(options) is the factory.
OpenAPIRegistryImplThe low-level store of paths, components and metadata.Used by both of the above. Reach for it only if you need direct control.
OpenAPIRouteScannerImplHolds routes and converts them to operations.addRoute, setRoute, removeRoute, scan, clear.
OpenAPIValidatorImplValidates a finished document.createOpenAPIValidator() is the factory.
SchemaRegistryImplConverts and stores named component schemas.Collects conversion warnings per name.

OpenAPIManager methods

NameWhat it doesNotes
addRoute(route)Registers a route.Throws on a duplicate method + OpenAPI path template.
setRoute(route)Registers a route, replacing any existing one.
removeRoute(method, path)Removes a route.Returns whether one was removed.
setRoutes(routes)Replaces the whole route set.Rejects duplicates before anything changes. Chainable.
routeWarnings()Warnings from the last route conversion.For example a declared path parameter the path lacks, or a route with no documented responses.
addSchema(name, schema)Converts a @zudojs/schema schema and registers it.Conversion warnings land in schemaWarnings().
addRawSchema(name, schema)Registers an already-converted OpenAPI schema.No conversion.
setInfo, addServer, addTagSet document metadata.Chainable.
addSecurityScheme(name, scheme)Declares an authentication scheme.Pair with addSecurityRequirement.
addSecurityRequirement(req)Requires a scheme document-wide.Validated against declared schemes.
generate(validate?)Builds the document.Idempotent. true throws on an invalid result.
getDocument(validate?)Returns the document, using the cache when fresh.Rebuilds when stale or absent.
validate()Validates without throwing.Returns { valid, errors, warnings }.
toJSON(validate?) / toYAML(validate?)Serializes the document.
toResponse(options?)HTTP response carrying the document.format, validate, cacheControl.
toUIResponse(options)HTTP response carrying a documentation page.Takes the renderOpenAPIUI options.
schemaWarnings()Conversion warnings, keyed by component name.Read-only map.
invalidateCache()Drops the cached document.Every mutation calls it for you.
reset()Drops every route, component and the cache.Use instead of the deprecated invalidate().
versionThe version this manager emits.Getter.

Functions

NameWhat it doesNotes
createOpenAPIDocumentFromRoutes(routes, options)Generates a document from route descriptors.See Generating from a route table.
createOpenAPIManagerFromRoutes(routes, options)The same, returning the manager.For toResponse / toUIResponse.
routeDescriptorToRouteInfo(descriptor)Descriptor to { method, path, metadata }.Throws OpenAPIRouteError on an unsupported method.
renderOpenAPIUI(options)Returns a complete HTML documentation page.Swagger UI or ReDoc.
zudoLogo(overrides?)The default logo object.Frozen; pass overrides for a variant.
svgToDataUri(svg)Encodes an SVG string as a compact data URI.No network request needed to show it.
convertSchema(schema, options?)Converts one schema without a registry.Returns { schema, warnings }.
createSchemaConverter(options?)A reusable converter bound to options.
isVersion31(version)Whether a version string is 3.1.x.
createComponentReference(section, name)Builds a $ref object.Escapes names per RFC 6901.
escapeJsonPointerSegment / unescapeJsonPointerSegmentEscape and unescape one pointer segment.~ and /.
toOpenAPIPath(path)/users/:id/users/{id}.Throws on wildcard or optional segments.
extractPathParameters(path)Parameter names in a path template.
convertRouteToOpenAPI(method, path, metadata?)Turns one route into an operation.
buildResponses(metadata?)The responses object for an operation.When none are declared, returns { default: { description: "Undocumented response" } }; it never invents a 200.
isOpenAPIMethod(method)Whether a string is an OpenAPI method.Type guard.
toOpenAPIJSON(document) / toOpenAPIYAML(document)Serialize a document.YAML quotes ambiguous strings.
createOpenAPIError / isOpenAPIError / formatIssuePathError helpers.formatIssuePath renders an issue path as paths./orders.get.

Types

NameWhat it doesNotes
OpenAPIManagerOptionsConstructor options.version, info, servers, tags, security, cacheTtlMs, onSchemaWarning, onRouteWarning, branding, now.
OpenAPIUIOptionsOptions for the documentation page.See the table in Serving.
OpenAPIUIRenderer"swagger" | "redoc".
OpenAPIUIResponse{ status, headers, body } for the page.Returned by toUIResponse.
OpenAPIDocumentResponse{ status, headers, body } for the spec.Returned by toResponse.
OpenAPILogo{ url, href?, altText?, backgroundColor? }.The shape of info["x-logo"].
RouteInfo / RouteMetadata / RouteOpenAPIMetadata / RouteParameterMetadataWhat addRoute accepts.
OpenAPIRouteDescriptor / OpenAPIDocumentFromRoutesOptionsInput to createOpenAPIDocumentFromRoutes.Options: info (required), validate, securitySchemes, schemas, plus the manager options.
OpenAPIValidationResult / OpenAPIValidationIssueValidator output.An issue is { path, message, severity }.
SchemaConversionResult / SchemaConversionOptionsConverter input and output.
OpenAPIDocument, OpenAPIOperation, OpenAPISchema, …The specification object types.Mirror the OpenAPI standard; all exported from the package root.

Errors

All extend OpenAPIError, the @zudojs/errors class re-exported here (a BaseError). They default to status 500 and are not exposed to clients — these are failures while your service builds its own specification, not answers to a request.

NameThrown whenNotes
OpenAPIValidationErrorA document fails validation.Carries issues and a format() summary.
OpenAPIRouteErrorA path or method cannot become an operation.Wildcards, optional parameters, unsupported methods.
OpenAPISchemaErrorA schema cannot be converted.Includes unresolvable recursion.
OpenAPIComponentConflictErrorA component name is registered twice with different content.Extends OpenAPIComponentError.
OpenAPIVersionErrorAn unsupported version is requested.See SUPPORTED_OPENAPI_VERSIONS.
OpenAPIDocumentError, OpenAPIComponentError, OpenAPIReferenceError, OpenAPISerializationError, OpenAPIOperationErrorThe remaining failure kinds.Each accepts statusCode and expose overrides.

Constants

NameValueNotes
DEFAULT_OPENAPI_VERSION"3.1.0"Used when you pass no version.
SUPPORTED_OPENAPI_VERSIONS3.0.03.0.3, 3.1.0, 3.1.1Anything else is rejected.
MAX_OPERATION_ID_LENGTH128Longer ids fail validation.
COMPONENT_REF_PREFIX"#/components"Prefix of every local $ref.
DEFAULT_MEDIA_TYPE"application/json"Content type of toResponse().
UNDOCUMENTED_RESPONSE_DESCRIPTION"Undocumented response"Description of the default response a route with none gets.
DEFAULT_SERVER_URL"http://localhost"Fallback server URL.
DOCUMENT_CACHE_TTL_MS300000Five minutes. Override with cacheTtlMs.
STATUS_CODE_CATEGORIES1XX5XXThe range keys OpenAPI allows.
RESPONSE_KEY_PATTERNRegExpWhat a valid response key looks like.
PATH_TEMPLATE_PARAMETERRegExpMatches {name} in a path.
ZUDO_SITE_URL and the ZUDO_* assetsSVG strings and data URIsSee Branding.

COMMON MISTAKES

  • Leaving out the responses.
    A route with no responses is published with a default “Undocumented response”, and every generated client has to guess what comes back. Declare the real ones, such as "204" for a delete, and listen to onRouteWarning so a new undocumented route shows up in your logs.
  • Serving the spec but not a page, or a page but not the spec.
    A toUIResponse page fetches specUrl at load time and shows an error if nothing answers. Register both handlers, and make specUrl match the route the spec is actually on.
  • Assuming a 3.1 document still means the same thing as 3.0.
    Copying a document between versions silently drops exclusive bounds and nullability. Set version on the manager and let the converter emit the right spelling.
  • Ignoring schema warnings.
    A dropped constraint — an unknown string format, a regex flag OpenAPI cannot express — is not an error, so nothing stops the build. Pass onSchemaWarning or read schemaWarnings() and log them.
  • Pasting a $ref from an untrusted document.
    A non-http(s) ref is now an error, but an https one is only a warning — legal OpenAPI, and still a fetch your resolver will perform. Bundle the target into components when the source is not yours.
  • Calling the deprecated invalidate() expecting a cache drop.
    It clears every registered route as well. Use invalidateCache() for the cache, reset() when you really mean to empty the manager.

COMPLETE EXPORT INDEX

Every name @zudojs/openapi exports from its package root at v1.5.0 — 138 in total, generated from the package’s own entry point rather than written by hand. The sections above explain the ones you reach for most; this is the exhaustive list, so nothing shipped is undocumented. Names not covered above are typically internal helpers and supporting types.

Show all 138 exports
Classes (17)
OpenAPIComponentConflictError OpenAPIComponentError OpenAPIDocumentBuilder OpenAPIDocumentError OpenAPIError OpenAPIManager OpenAPIOperationError OpenAPIReferenceError OpenAPIRegistryImpl OpenAPIRouteError OpenAPIRouteScannerImpl OpenAPISchemaError OpenAPISerializationError OpenAPIValidationError OpenAPIValidatorImpl OpenAPIVersionError SchemaRegistryImpl
Functions (32)
buildOpenAPIUIContentSecurityPolicy buildOperationParameters buildOperationRequestBody buildOperationResponses buildResponses convertRouteToOpenAPI convertSchema createComponentReference createOpenAPIDocumentBuilder createOpenAPIDocumentFromRoutes createOpenAPIError createOpenAPIManager createOpenAPIManagerFromRoutes createOpenAPIValidator createSchemaConverter describeResponseKey escapeJsonPointerSegment extractPathParameters formatIssuePath isOpenAPIError isOpenAPIMethod isSchemaDefinition isVersion31 renderOpenAPIUI resolveSchemaInput routeDescriptorToRouteInfo svgToDataUri toOpenAPIJSON toOpenAPIPath toOpenAPIYAML unescapeJsonPointerSegment zudoLogo
Interfaces (55)
OpenAPIComponentRegistration OpenAPIComponents OpenAPIContact OpenAPIDiscriminator OpenAPIDocument OpenAPIDocumentFromRoutesOptions OpenAPIDocumentOptions OpenAPIDocumentResponse OpenAPIEncoding OpenAPIErrorOptions OpenAPIExample OpenAPIExternalDocumentation OpenAPIHeader OpenAPIInfo OpenAPILicense OpenAPILink OpenAPILogo OpenAPIManagerOptions OpenAPIMediaType OpenAPIOAuthFlow OpenAPIOAuthFlows OpenAPIOperation OpenAPIParameter OpenAPIPathItem OpenAPIReference OpenAPIRegistry OpenAPIRequestBody OpenAPIResponse OpenAPIRoute OpenAPIRouteBody OpenAPIRouteDescriptor OpenAPIRouteResponse OpenAPISchema OpenAPISecurityRequirement OpenAPISecurityScheme OpenAPIServer OpenAPIServerVariable OpenAPITag OpenAPIUIAssetIntegrity OpenAPIUIOptions OpenAPIUIResponse OpenAPIValidationIssue OpenAPIValidationResult OpenAPIValidator OpenAPIXml RouteInfo RouteMetadata RouteOpenAPIMetadata RouteParameterMetadata SchemaConversionOptions SchemaConversionResult SchemaConverter SchemaInputOptions SchemaRegistry SchemaRegistryOptions
Type aliases (9)
ComponentSection OpenAPIHttpMethod OpenAPIParameterLocation OpenAPIPaths OpenAPIResponses OpenAPISchemaInput OpenAPIUIRenderer OpenAPIVersion RouteConversionOptions
Constants (25)
COMPONENT_REF_PREFIX DEFAULT_MEDIA_TYPE DEFAULT_OPENAPI_VERSION DEFAULT_SERVER_URL DOCUMENT_CACHE_TTL_MS MAX_OPERATION_ID_LENGTH PATH_TEMPLATE_PARAMETER REDOC_VERSION RESPONSE_KEY_PATTERN STATUS_CODE_CATEGORIES SUPPORTED_OPENAPI_VERSIONS SWAGGER_UI_VERSION UNDOCUMENTED_RESPONSE_DESCRIPTION ZUDO_FAVICON_DATA_URI ZUDO_FAVICON_SVG ZUDO_MARK_DARK_DATA_URI ZUDO_MARK_DARK_SVG ZUDO_MARK_DATA_URI ZUDO_MARK_SVG ZUDO_SITE_URL ZUDO_WORDMARK_DARK_DATA_URI ZUDO_WORDMARK_DARK_SVG ZUDO_WORDMARK_DATA_URI ZUDO_WORDMARK_SVG ZUDOLIB_TO_OPENAPI_METHODS