Skip to main content

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.json
  • GET /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.list
  • User.createAdvanced
  • User.read
  • User.readByFilterAdvanced
  • User.updateAdvanced
  • User.members.list
  • root.query

Swagger UI Options​

createOpenApiRouter(...) accepts:

  • title
  • version
  • description
  • servers
  • jsonPath
  • docsPath
  • swaggerUiCssUrl
  • swaggerUiBundleUrl

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 + path with a non-equivalent descriptor throws OpenApiCollisionError, and reusing an operationId already bound to another route throws as well. Equivalent re-registrations marked with idempotent: true (for example the root batch route root.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: true on the new descriptor. allowReplace still validates operation-ID uniqueness, so it cannot be used to steal another route's operationId.
  • Low-level callers that need legacy silent-replacement behavior for migration can construct OpenApiRegistry directly 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 custom jsonPath are 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.