English 한국어
Descriptor-driven OpenAPI 3.1.0 document generation for fluo, with standard decorators for explicit documentation metadata and optional Swagger UI support.
pnpm add @fluojs/openapi@fluojs/openapi supports Node.js >=24.0.0 <27 and declares that exact range through engines.node. That package-owned support contract excludes Node versions below 24 and Node 27+; portable @fluojs/runtime has no package-wide Node engine. Earlier 1.x releases advertised engines.node >=20.0.0.
- When you want to provide interactive documentation for your REST API using Swagger UI.
- When you need a machine-readable OpenAPI 3.1.0 specification for client generation or testing.
- When you want to keep your API documentation in sync with your code using standard decorators.
- When you need to derive request models from DTO binding/validation metadata and declare response models explicitly.
- When one application needs separate JSON and UI routes for multiple API versions or audiences.
Register the OpenApiModule and pass sources, prebuilt descriptors, or both so the document builder knows which HTTP handlers to include. When both inputs are provided, they are merged.
import { FluoFactory } from '@fluojs/runtime';
import { Controller, Get } from '@fluojs/http';
import { Module } from '@fluojs/core';
import { NodeHttpApplicationAdapter, createConsoleApplicationLogger } from '@fluojs/platform-nodejs';
import { OpenApiDocumentBuilder, OpenApiModule, ApiOperation, ApiResponse, ApiTag } from '@fluojs/openapi';
@ApiTag('Users')
@Controller('/users')
class UsersController {
@ApiOperation({ summary: 'List all users' })
@ApiResponse({ status: 200, description: 'Success' })
@Get('/')
list() {
return [];
}
}
@Module({
imports: [
OpenApiModule.forRoot({
sources: [{ controllerToken: UsersController }],
title: 'My API',
version: '1.0.0',
ui: true, // Enable Swagger UI at /docs
})
],
controllers: [UsersController]
})
class AppModule { }
const app = await FluoFactory.create(AppModule, {
adapter: NodeHttpApplicationAdapter.create({ port: 3000 }),
logger: createConsoleApplicationLogger(),
});
await app.listen();
// OpenAPI JSON: http://localhost:3000/openapi.json
// Swagger UI: http://localhost:3000/docsIf you need to bypass controller discovery, create handler descriptors with createHandlerMapping(...) from @fluojs/http and pass them through descriptors. OpenApiModule does not infer handlers from @Module({ controllers: [...] }) on its own.
When a prebuilt descriptor and a discovered source resolve to the same OpenAPI path and HTTP method, the later descriptor wins. Because OpenApiModule composes discovered sources first and explicit descriptors second, explicit descriptors take precedence without emitting duplicate operations or silently leaving stale source metadata in the generated document.
@ApiOperation() / @ApiOperation(undefined) and @ApiBody() / @ApiBody(undefined)
use the existing {} semantics. No summary, description, deprecated flag, required flag,
or schema is invented. Empty body metadata preserves a DTO-inferred body and adds no
requestBody when none is inferred. Empty writes can overwrite earlier stacked metadata,
so these calls are not always equivalent to omitting the decorator. Existing application-time
null failures remain. ApiTag(tag), object-only ApiResponse({ status, ... }), and parameter/security
names remain required; the supported OpenAPI Path Item methods do not change.
fluo inspects only the controllers and handler descriptors supplied through sources and descriptors to build an OpenAPI 3.1.0 document. This includes paths, methods, parameters, and request bodies for that explicit input set; importing a controller into an application module does not add it automatically.
The builder emits only standard Path Item operations: get, put, post, delete, options, head, patch, and trace. Fluo catch-all ALL descriptors are runtime routing inputs, not OpenAPI operations, so document generation rejects them instead of serializing a nonstandard all key. Unsupported descriptor methods fail with the same path-specific error.
After documentTransform, every Path Item is validated again. Transforms may use the OpenAPI 3.1 operations (including trace), fixed fields ($ref, summary, description, servers, and parameters), and x-* specification extensions. Keys such as all, query, or other unknown fields fail document generation before the document is exposed.
Before upgrading, replace every @All() route or custom handler descriptor with separate supported HTTP method routes, or exclude it from OpenAPI input. In documentTransform, remove nonstandard Path Item keys such as all and query; retain only standard operations, fixed fields, and x-* extensions. Unsupported input now throws during document generation instead of emitting an invalid document.
When an HTTP handler declares @Produces(...) from @fluojs/http, generated OpenAPI responses use those media types as the response content keys. For example, @Produces('application/json', 'application/problem+json') on a handler with an @ApiResponse(...) schema emits both media types with the same response schema instead of silently falling back to only application/json.
When a handler does not declare @ApiResponse(...) or @HttpCode(...), the OpenAPI builder applies method-only implicit defaults: POST handlers default to 201, and other methods default to 200. Bodyless or runtime-dependent cases such as DELETE and OPTIONS should declare the intended success status explicitly with @HttpCode(...) or @ApiResponse(...).
The builder does not inspect handler return values or TypeScript return types to infer response content. A default success response contains only its status and the description OK. Add @ApiResponse(...) with schema or type when the OpenAPI document must describe a response body; without either field, an explicit response still contains status and description only.
Works with @fluojs/validation to derive request schemas from DTO binding and validation metadata. Response DTOs become OpenAPI components only when they are referenced explicitly, such as with @ApiResponse({ status, type: ResponseDto }) or extraModels.
For generated request schemas, repeated Min rules fold the strongest lower
bound with Math.max and repeated Max rules fold the strongest upper bound
with Math.min; Length, MinLength, and MaxLength combine into the strongest
minLength/maxLength bounds; ArrayNotEmpty, ArrayMinSize, and
ArrayMaxSize do the same for minItems/maxItems. IsIn and IsEnum emit
the deduped intersection of their allowed values, emitting an impossible schema
(not: {}) for disjoint constraints. Distinct ValidateNested targets are
composed deterministically (such as with allOf under IntersectionType), with
{ each: true } taking array-schema precedence when both nested forms are
present. These are OpenAPI projection rules only: runtime nested collection
traversal remains owned by @fluojs/validation.
OpenApiSchemaObject accepts finite OpenAPI 3.1 numeric exclusiveMinimum and exclusiveMaximum values. Legacy boolean exclusiveMinimum and exclusiveMaximum inputs are rejected, including when an untyped documentTransform introduces them.
OpenApiSchemaObject rejects legacy nullable. Use a type union such as ['string', 'null'], or an anyOf branch with { type: 'null' }; those OpenAPI 3.1 forms are preserved after documentTransform.
Handles URI-based versioning from @fluojs/http automatically. Your OpenAPI paths will correctly reflect the resolved versioned routes.
Easily document authentication requirements like Bearer tokens or API keys using @ApiBearerAuth() and @ApiSecurity(name, scopes?).
Stacking multiple @ApiSecurity(name, scopes?) decorators for the same scheme merges scopes into one cumulative OpenAPI security requirement for that scheme. This keeps OAuth-style requirements deterministic when a route declares overlapping scopes such as ['reports:read'] and ['reports:write', 'reports:read'], while different schemes remain separate requirements.
When ui: true is enabled, the generated /docs page references an exact swagger-ui-dist asset version so release behavior stays deterministic across package updates. If your deployment requires self-hosted assets for offline or CSP-controlled environments, set swaggerUiAssets.cssUrl and swaggerUiAssets.jsBundleUrl; the generated HTML escapes those URLs and does not expose the Swagger UI instance on window.ui.
Each OpenApiModule registration serves JSON at documentPath and reserves its Swagger UI route at uiPath. The defaults remain /openapi.json and /docs, so existing applications do not need configuration changes. Set both paths when one application imports multiple OpenAPI modules:
@Module({
imports: [
OpenApiModule.forRoot({
documentPath: '/openapi/public.json',
sources: [{ controllerToken: PublicController }],
title: 'Public API',
ui: true,
uiPath: '/docs/public',
version: '1.0.0',
}),
OpenApiModule.forRoot({
documentPath: '/openapi/admin.json',
sources: [{ controllerToken: AdminController }],
title: 'Admin API',
ui: true,
uiPath: '/docs/admin',
version: '1.0.0',
}),
],
})
class AppModule {}Paths follow the @fluojs/http route grammar and normalize duplicate or trailing slashes. Route collisions do not use document-descriptor precedence: if two normalized GET routes collide—between JSON and UI paths, separate OpenAPI modules, or another application controller—application bootstrap fails with RouteConflictError. The UI route remains reserved when ui is false so the configured endpoint can return the documented Swagger UI is disabled. not-found response.
OpenApiModule.forRoot(...) snapshots and freezes its options at registration time. Mutating the original options object, documentPath, uiPath, sources, descriptors, securitySchemes, extraModels, or swaggerUiAssets after registration does not alter the served OpenAPI document or UI HTML. The generated singleton document is also served through defensive copies, so downstream response serialization or tests cannot mutate the stored document for later requests. OpenApiModule.forRootAsync(...) fixes documentPath and uiPath from the outer registration before module compilation, applies the same snapshot once the async document-options factory resolves, and propagates factory failures during bootstrap.
Use OpenApiModule.forRootAsync(...) when title/version/source configuration comes from DI or async setup. Put registration-time documentPath and uiPath beside inject and useFactory; return sources, descriptors, securitySchemes, extraModels, defaultErrorResponsesPolicy, documentTransform, ui, and swaggerUiAssets from the factory. defaultErrorResponsesPolicy defaults to injecting standard error responses and an ErrorResponse schema, while documentTransform runs after document generation and before serving.
Generated documents are not a one-to-one NestJS Swagger compatibility layer. By default, fluo adds 400, 401, 403, 404, and 500 responses that do not replace explicitly declared responses, and includes the shared ErrorResponse schema. Verify the generated error contract before regenerating clients; set defaultErrorResponsesPolicy: 'omit' when the legacy document must not receive those default responses.
Fluo derives each operationId deterministically from the controller tag, handler name, HTTP method, and normalized path. Collisions receive numeric suffixes. If generated clients rely on legacy identifiers, rename the generated operation IDs in documentTransform before the document is served, then verify the transformed document with the client generator.
With forRootAsync(...), documentPath and uiPath are outer registration options because their routes are compiled before useFactory(...) resolves. Keep those paths beside inject and useFactory; return only document configuration from the factory. A path returned by the factory cannot reconfigure the already-registered routes.
OpenApiModule: Main entry point for OpenAPI integration.ApiTag,ApiOperation,ApiResponse: Documentation decorators.ApiBody,ApiParam,ApiQuery,ApiHeader,ApiCookie: Explicit request-body and parameter documentation decorators that override inferred request documentation when names overlap.ApiBearerAuth,ApiSecurity: Security requirement decorators.ApiExcludeEndpoint: Omit specific handlers from documentation.ApiOperationOptions,ApiResponseOptions,ApiParameterOptions,ApiBodyOptions: Decorator option types accepted by@ApiOperation(...),@ApiResponse(...),@ApiParam(...),@ApiQuery(...),@ApiHeader(...),@ApiCookie(...), and@ApiBody(...).OpenApiDocumentBuilder: Programmatic offline document builder; callOpenApiDocumentBuilder.build(options).getControllerTags,getMethodApiMetadata: Metadata readers for advanced tests and integration tooling.OpenApiModuleOptions,OpenApiAsyncModuleOptions,OpenApiRouteOptions,OpenApiSwaggerUiAssetsOptions,OpenApiDocumentBuilderOptions,DefaultErrorResponsesPolicy: Option types for module and builder integrations.OpenApiDocument,OpenApiSecuritySchemeObject, and related OpenAPI shape types: Typed document surface for tests, tooling, and integrations.OpenApiSchemaObject: Typed schema surface for explicit@ApiBody(...)and@ApiResponse(...)schemas, including OpenAPI 3.1 composition (allOf,oneOf,anyOf), null unions, finite exclusive bounds, object/array constraints, examples/defaults, and read/write/deprecated annotations. Legacynullableand boolean exclusive bounds are rejected.
@fluojs/core: Shared metadata utilities.@fluojs/http: Controller and routing integration.@fluojs/validation: Schema and model generation from DTOs.
packages/openapi/src/openapi-module.test.ts: Integration tests and usage examples.packages/openapi/src/openapi-module-routes.test.ts: Default, custom, multi-document, and route-collision examples.packages/openapi/src/schema-builder.test.ts: Document builder and schema generation examples.
- Replace
buildOpenApiDocument(options)withOpenApiDocumentBuilder.build(options)and replaceBuildOpenApiDocumentOptionswithOpenApiDocumentBuilderOptions. - Remove
OpenApiHandlerRegistry; passsourcesanddescriptorsdirectly to the builder orOpenApiModule. - Use object-only
@ApiResponse({ status, ...options }). - Replace
@ApiBody({ schema })with@ApiBody({ content: { 'application/json': { schema } } }). - Replace
nullablewith a null type union oranyOf, and replace boolean exclusive bounds with finite numeric values.