@dunx/openapi

OpenAPI 3.1 documents for dunx controllers, generated from the schemas the routes already validate, with Swagger UI or Scalar mounted over them

@dunx/openapi
@dunx/openapi/scalar
@dunx/openapi/swagger

An OpenAPI 3.1 document generated from the routes an app already has, with Swagger UI or Scalar served from the same Bun.serve as everything else.

The difference from a reflection-based generator is what it reads. A route's schemas are the objects the request path validates against. Its security metadata (@Public(), @Roles()) is what its guards enforce. Both are already on the route, so the document is not a second description that can drift from the first.

Usage

TS
import { HttpFactory } from '@dunx/http';
import { OpenApiModule } from '@dunx/openapi';
import { SwaggerRenderer } from '@dunx/openapi/swagger';

const app = await HttpFactory.create(
  OpenApiModule.forRoot({
    title: 'Payments',
    version: '1.4.0',
    root: AppModule, // the graph to document, and the graph that gets imported
    // Omit for the document alone: no `renderer`, no page route.
    renderer: new SwaggerRenderer({ docExpansion: 'list' }),
  }),
);
app.setGlobalPrefix('api');
await app.listen(3000);
// GET /api/docs          the explorer
// GET /api/openapi.json  the document

forRootAsync takes a factory that may inject, so an app can read its title off ConfigService or contribute another library's schema.

What is here

The OpenAPI guide is canonical.

PieceWhat it does
OpenApiModuleWraps the root it documents, mounts the JSON and a renderer's page
DocsRendererThe page contract. PackageAssets serves a renderer's files
@ApiDocSummary, description, tags and deprecation on a route or a class
@ApiHiddenA real route kept out of the document. Lives in @dunx/http
describeRoutesThe routes as data, constructing nothing
generateDocumentThe document from those routes
betterAuthDocumentbetter-auth's own paths merged in. Lives in @dunx/auth
bunx dunx-openapiThe document written to a file, with no container and no server

Security comes from the same @Public() and @Roles() metadata the guards read at runtime, so there is no second annotation for the document to disagree with. A response schema is documentation only: the verb decorators hold the handler's return type to it at compile time instead of validating every response.

authorize decides who may see the document, the page and the page's assets. It takes the same Authorize as @dunx/dashboard. A refused request gets a 404. If authorize returns a Response, for example a redirect to sign in, that response is sent as is.

Notes

  • Add .meta({ id }) to a zod schema to put it in components/schemas. Without an id it is inlined wherever it is used. .strict() after .meta() drops the metadata, so call .meta() last.
  • Prose belongs in description. An explorer labels a schema by title, which @dunx/openapi fills with the component name.
  • The page embeds the document rather than fetching it, and loads the renderer's files same-origin from your own install. Nothing reaches a CDN, unless you turn Scalar's withDefaultFonts back on.
  • renderer sits beside root rather than in forRootAsync's factory: the controller declares its routes before there is a container to run one.