OpenAPI
access-router registers generated routes into an internal OpenAPI registry and can expose that registry as JSON and Swagger UI.
Quick Start
import express from 'express';
import acl from '@web-ts-toolkit/access-router';
const app = express();
const userRouter = acl.createRouter('User', {
basePath: '/users',
});
app.use(userRouter.routes);
app.use(
acl.createOpenApiRouter({
title: 'Example API',
version: '1.0.0',
}),
);
Default routes:
GET /openapi.jsonGET /docs
What Gets Documented
Generated documentation includes:
- model router endpoints
- data router endpoints
- root batch router endpoints
- generated path parameters, query parameters, and request bodies
- default success and error responses
- route tags and operation ids
Paths are emitted as OpenAPI-style paths such as /users/{id}.
Server-only authorization is not request schema
Explicit service policies and opaque document handles belong to trusted backend code,
not HTTP/OpenAPI/client/React options. Generated bodies/root entries and nested include,
populate and $$sq control slots reject authorization, removed service overrides
and private execution/prepared keys with BadRequest, including after custom validators
transform input. Do not add them to custom request-schema metadata. Ordinary persisted
business properties with those names remain data under normal field rules.
Response permission metadata describes the request user, not temporary backend grants; internal contexts/handles are not serialized. See Services for the separate service API and installed-package examples.
Operation-access variants and exposure
operationAccess denial returns HTTP 401 Unauthorized; it does not omit
endpoints. Basic and advanced model/data/subdocument routes remain in OpenAPI and
router.router.getEndpoints(), even with false guards. Permission setters are
live and do not require route reconstruction. Express HEAD fallback follows the
corresponding basic GET guard. See the routing matrix.
Variant keys do not rename existing operation IDs or become root batch op
values. Root entries continue to use base operation names/checks, with entry 401
inside the HTTP 200 batch envelope. Generated error responses include 401.
Built-in direct/root bodies that support options.populateAccess document the
same 'list' | 'read' enum they validate; populate descriptor access has that enum
too. Route variant selectors are invalid data-policy access. The shared Core
populate boundary also rejects reserved variants in nested generic options or
descriptors before target persistence, with controlled direct 400/root entry
400 bad_request (see Services).
Request bodies that accept include (advanced list/read bodies and root
batch entries) document both the legacy localField/foreignField shape
and the correlated shape: mode: 'correlated' with model, op
(read/list/count), explicit output path, exactly one of id or
filter, and per-operation args. { "$parent": "<field>" } markers and
{ "$escape": ... } literals appear as plain JSON objects in the schema —
they carry no special OpenAPI type beyond the shared include schema, which
reuses the same validation (includeItemSchema) that the routes enforce,
so the documented shape and the runtime verdicts agree.
Operation IDs
Generated operation ids follow the router naming conventions, for example:
User.listUser.createAdvancedUser.readUser.readByFilterAdvancedUser.updateAdvancedUser.members.listroot.query
Swagger UI Options
createOpenApiRouter(...) accepts:
titleversiondescriptionserversjsonPathdocsPathswaggerUiCssUrlswaggerUiBundleUrl
Example with custom paths:
app.use(
acl.createOpenApiRouter({
title: 'Internal API',
version: '2026-06-01',
jsonPath: '/spec/openapi.json',
docsPath: '/reference',
}),
);
JSON-only mode:
app.use(
acl.createOpenApiRouter({
title: 'Internal API',
version: '1.0.0',
docsPath: false,
}),
);
Custom Validation Schemas
Built-in request schemas are translated automatically.
For custom validators, attach explicit OpenAPI metadata with defineRequestSchema(...):
import { defineRequestSchema } from '@web-ts-toolkit/access-router';
const advancedCreateSchema = defineRequestSchema(
async (value) => {
return { success: true, data: value };
},
{
openapi: {
type: 'object',
properties: {
data: {
type: 'object',
additionalProperties: true,
},
},
},
},
);
Response Metadata
If a route does not override responses manually, access-router derives default responses from route metadata:
- list-like routes get list-style responses
- create routes get
201 Created - delete routes get delete-style responses
- other routes get single-resource responses
Default error responses are documented with application/problem+json payloads.
Notes
- The generated document is OpenAPI
3.1.0. - By default the OpenAPI registry is strict: registering the same
method+pathwith a non-equivalent descriptor throwsOpenApiCollisionError, and reusing anoperationIdalready bound to another route throws as well. Equivalent re-registrations marked withidempotent: true(for example the root batch routeroot.query) are still accepted as no-ops, so mounting the same routers more than once on the same runtime is safe. - To override an existing descriptor intentionally, set
allowReplace: trueon the new descriptor.allowReplacestill validates operation-ID uniqueness, so it cannot be used to steal another route'soperationId. - Low-level callers that need legacy silent-replacement behavior for migration
can construct
OpenApiRegistrydirectly with{ rejectConflicts: false, rejectDuplicateOperationIds: false }. - The inline Swagger UI script safely embeds the spec path; characters such as
</script>, quotes, ampersands, and the U+2028 / U+2029 line separators in a customjsonPathare escaped so they remain data and cannot terminate the<script>element. - The OpenAPI router documents generated routes only; your custom Express endpoints need separate registration if you want them in the same spec.