Libraries
    Preparing search index...

    Module @cleverbrush/server-openapi

    @cleverbrush/server-openapi

    CI License: BSD-3-Clause

    OpenAPI 3.1 specification generation for @cleverbrush/server. Converts endpoint registrations, schema definitions, and authentication configuration into a fully-formed OpenAPI document — no annotations, no decorators. Also generates AsyncAPI 3.0 documents for WebSocket subscription endpoints.

    • generateOpenApiSpec() — converts @cleverbrush/server endpoint registrations into an OpenAPI 3.1 document.
    • generateAsyncApiSpec() — converts @cleverbrush/server WebSocket subscription registrations into an AsyncAPI 3.0 document.
    • serveAsyncApi() — middleware that lazily generates and caches the AsyncAPI spec; serves it at a configurable path (default: /asyncapi.json).
    • Schema conversion — maps @cleverbrush/schema builders to JSON Schema Draft 2020-12 via @cleverbrush/schema-json.
    • Path resolution — converts both colon-style paths (:id) and ParseStringSchemaBuilder templates to OpenAPI {param} format with per-parameter schemas.
    • Security mapping — translates @cleverbrush/auth authentication schemes to OpenAPI securitySchemes; maps per-endpoint authorize() to security arrays. Auto-detects OAuth 2.0 flows (authorizationCodeScheme, clientCredentialsScheme) and OpenID Connect (oidcScheme).
    • Top-level tags — pass tags: [{ name, description?, externalDocs? }] to OpenApiOptions; tag names are also auto-collected from endpoint registrations.
    • Request body examples — emit example / examples on Media Type Objects via .example() and .examples() on EndpointBuilder. Schema-level examples propagate automatically.
    • Binary / file responses.producesFile(contentType?, description?) emits binary content types instead of JSON schemas for file download endpoints.
    • Multiple content types.produces({ 'text/csv': {}, 'application/xml': { schema } }) emits a multi-entry content map for content-negotiated endpoints; an optional per-type schema override is supported.
    • Response headers.responseHeaders(schema) documents response headers (X-Total-Count, rate-limit, cache-control, etc.) across every response code; each property becomes a named header entry with schema and description.
    • External docs.externalDocs(url, description?) attaches an externalDocs object to the OpenAPI Operation Object.
    • Links.links(defs) declares follow-up links from a response; emitted under the primary 2xx response's links map. Parameters can be raw runtime expression strings or a type-safe callback (response) => Record<string, unknown> where property accesses are resolved to $response.body#/<pointer> expressions automatically.
    • Callbacks.callbacks(defs) declares async out-of-band callbacks on the Operation Object. The callback URL can be a raw expression string or a type-safe urlFrom callback resolved from the request body schema via property descriptors.
    • Webhooks — pass webhooks: [defineWebhook('name', options)] to OpenApiOptions (and register via ServerBuilder.webhook(def)) to emit a top-level webhooks map in the OpenAPI document.
    • serveOpenApi() — middleware that lazily generates and caches the spec; serves it at a configurable path (default: /openapi.json).
    • createOpenApiEndpoint() — returns a typed endpoint + handler pair for use with ServerBuilder.handle().
    • CLI / build scriptwriteOpenApiSpec() writes the spec to a file.
    npm install @cleverbrush/server-openapi @cleverbrush/server @cleverbrush/schema
    
    import { ServerBuilder, endpoint } from '@cleverbrush/server';
    import { serveOpenApi } from '@cleverbrush/server-openapi';
    import { object, string, number } from '@cleverbrush/schema';

    const GetUser = endpoint
    .get('/api/users/:id')
    .summary('Get a user by ID')
    .tags('users');

    const server = new ServerBuilder();

    server
    .use(serveOpenApi({
    server,
    info: { title: 'My API', version: '1.0.0' }
    }))
    .handle(GetUser, ({ params }) => ({ id: params.id }));

    await server.listen(3000);
    // GET /openapi.json → OpenAPI 3.1 document

    When server is provided, endpoint registrations, authentication config, and webhooks are derived automatically. You can still pass getRegistrations, authConfig, or webhooks explicitly to override any server-derived value.

    import { serveOpenApi } from '@cleverbrush/server-openapi';

    server.use(serveOpenApi({
    server,
    info: { title: 'My API', version: '1.0.0' },
    servers: [{ url: 'https://api.example.com', description: 'Production' }],
    path: '/openapi.json' // default
    }));
    import { createOpenApiEndpoint } from '@cleverbrush/server-openapi';

    const { endpoint: openApiEp, handler } = createOpenApiEndpoint({
    server,
    info: { title: 'My API', version: '1.0.0' }
    });

    server.handle(openApiEp, handler);
    import { writeOpenApiSpec } from '@cleverbrush/server-openapi';

    await writeOpenApiSpec({
    registrations: server.getRegistrations(),
    info: { title: 'My API', version: '1.0.0' },
    outputPath: './openapi.json'
    });

    When the same schema definition is used by multiple endpoints, you can mark it with .schemaName() from @cleverbrush/schema so that generateOpenApiSpec() extracts it once into components/schemas and replaces every inline occurrence with a $ref pointer.

    1. Call .schemaName('ComponentName') on any @cleverbrush/schema builder you want to extract.
    2. Export the result as a constant and reuse the same reference wherever the schema is needed.
    3. generateOpenApiSpec() detects all named schemas via a pre-pass walk, emits them under components.schemas, and replaces inline definitions with $ref pointers.
    import { object, string, number, array } from '@cleverbrush/schema';
    import { endpoint } from '@cleverbrush/server';
    import { generateOpenApiSpec } from '@cleverbrush/server-openapi';

    // Mark once — reuse everywhere
    const UserSchema = object({
    id: number(),
    name: string(),
    }).schemaName('User');

    const GetUser = endpoint.get('/api/users/:id').returns(UserSchema);
    const ListUsers = endpoint.get('/api/users').returns(array(UserSchema));

    const spec = generateOpenApiSpec({
    registrations: [GetUser.registration, ListUsers.registration],
    info: { title: 'My API', version: '1.0.0' }
    });
    // components.schemas.User → { type: 'object', properties: { id: …, name: … } }
    // GET /api/users/:id → responses.200.content['application/json'].schema: { $ref: '#/components/schemas/User' }
    // GET /api/users → responses.200.content['application/json'].schema: { type: 'array', items: { $ref: '…/User' } }

    Nested named schemas inside request bodies are also resolved:

    const AddressSchema = object({ street: string(), city: string() }).schemaName('Address');

    // The wrapper is anonymous — inlined. The nested AddressSchema → $ref.
    const CreateUserBody = object({ address: AddressSchema, name: string() });

    Registering two different schema instances under the same name throws immediately during spec generation:

    const A = object({ x: string() }).schemaName('Thing');
    const B = object({ y: number() }).schemaName('Thing'); // different instance!

    generateOpenApiSpec({ registrations: [...], info: { … } });
    // Error: Schema name "Thing" is already registered by a different schema instance.

    Re-registering the same instance (because it appears in multiple endpoints) is a no-op.

    SchemaRegistry and walkSchemas are also exported from @cleverbrush/server-openapi for custom tooling:

    import { SchemaRegistry, walkSchemas } from '@cleverbrush/server-openapi';

    const registry = new SchemaRegistry();
    walkSchemas(MySchema, registry);

    registry.getName(MySchema); // 'MyComponentName' | null
    registry.entries(); // IterableIterator<[name, SchemaBuilder]>
    registry.isEmpty; // boolean

    When a request body, response, or parameter schema is a discriminated union — all branches are objects sharing a required property with unique literal values — the generated spec automatically includes the OpenAPI discriminator keyword alongside anyOf.

    If the union branches use .schemaName() and are extracted as $ref components, the discriminator also includes a mapping from each literal value to its $ref path:

    const Cat = object({ type: string('cat'), name: string() }).schemaName('Cat');
    const Dog = object({ type: string('dog'), breed: string() }).schemaName('Dog');
    const PetBody = union(Cat).or(Dog);

    const CreatePet = endpoint.post('/api/pets').body(PetBody);

    // Generated spec:
    // requestBody.content['application/json'].schema:
    // {
    // anyOf: [{ $ref: '#/components/schemas/Cat' }, { $ref: '#/components/schemas/Dog' }],
    // discriminator: { propertyName: 'type', mapping: { cat: '#/components/schemas/Cat', dog: '#/components/schemas/Dog' } }
    // }

    Code generators like openapi-generator and orval use the discriminator to produce proper tagged union types.

    Self-referential schemas (tree nodes, nested menus, threaded comments) are supported via lazy() from @cleverbrush/schema. Call .schemaName() on the root schema and generateOpenApiSpec will:

    1. Register the named schema in components.schemas, expanding its definition exactly once.
    2. Replace every recursive reference inside the definition with the appropriate $ref pointer — breaking the cycle automatically.
    import { object, number, array, lazy } from '@cleverbrush/schema';

    type TreeNode = { value: number; children: TreeNode[] };

    // TypeScript needs an explicit annotation for recursive types
    const treeNode: ReturnType<typeof object> = object({
    value: number(),
    children: array(lazy(() => treeNode))
    }).schemaName('TreeNode');

    // Use treeNode as a body / response schema — no extra configuration needed
    const CreateTree = endpoint.post('/api/tree').body(treeNode);

    Generated spec (abbreviated):

    components:
    schemas:
    TreeNode:
    type: object
    properties:
    value: { type: integer }
    children:
    type: array
    items: { $ref: '#/components/schemas/TreeNode' }
    paths:
    /api/tree:
    post:
    requestBody:
    content:
    application/json:
    schema: { $ref: '#/components/schemas/TreeNode' }

    Pre-fill the Try it out panel in Swagger UI by attaching examples to endpoints.

    const CreateUser = endpoint
    .post('/api/users')
    .body(UserSchema)
    .example({ name: 'Alice', email: 'alice@example.com' });

    Emits example on the OpenAPI Media Type Object:

    requestBody:
    content:
    application/json:
    schema: { ... }
    example: { name: Alice, email: alice@example.com }
    const CreateUser = endpoint
    .post('/api/users')
    .body(UserSchema)
    .examples({
    minimal: { summary: 'Minimal', value: { name: 'Alice' } },
    full: {
    summary: 'Complete',
    description: 'A fully populated user',
    value: { name: 'Alice', email: 'alice@example.com', age: 30 }
    }
    });

    Examples attached directly to schemas via .example() propagate to parameter and response schemas in the generated spec:

    const PageParam = number().example(1);
    const UserResponse = object({ id: number(), name: string() }).example({ id: 1, name: 'Alice' });

    Use .producesFile() to declare that an endpoint returns a binary file instead of JSON. The generated spec emits the appropriate binary content type.

    const ExportCsv = endpoint
    .get('/api/export')
    .producesFile('text/csv', 'CSV export');

    const Download = endpoint
    .get('/api/download')
    .producesFile(); // defaults to application/octet-stream

    Produces:

    responses:
    '200':
    description: CSV export
    content:
    text/csv:
    schema: { type: string, format: binary }

    When both .returns() and .producesFile() are set, the binary response takes precedence.

    When you pass the server option, authentication configuration is picked up automatically from server.getAuthenticationConfig(). Security schemes and per-operation security arrays are generated without any extra configuration:

    import { jwtScheme } from '@cleverbrush/auth';

    const server = new ServerBuilder()
    .useAuthentication({
    defaultScheme: 'jwt',
    schemes: [jwtScheme({ secret: '...', mapClaims: c => c })]
    })
    .useAuthorization();

    server.use(serveOpenApi({
    server,
    info: { title: 'My API', version: '1.0.0' }
    }));

    You can also pass authConfig explicitly (useful when not using the server option):

    server.use(serveOpenApi({
    getRegistrations: () => server.getRegistrations(),
    info: { title: 'My API', version: '1.0.0' },
    authConfig: server.getAuthenticationConfig()
    }));

    JWT schemes generate { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }; cookie schemes generate { type: 'apiKey', in: 'cookie' }.

    OpenAPI supports a top-level tags array where each entry can carry a description and optional externalDocs. Pass a tags array to generateOpenApiSpec() (or any serving helper) to define them:

    generateOpenApiSpec({
    registrations,
    info: { title: 'My API', version: '1.0.0' },
    tags: [
    {
    name: 'users',
    description: 'User management endpoints',
    externalDocs: { url: 'https://docs.example.com/users' }
    },
    { name: 'orders', description: 'Order management endpoints' }
    ]
    });

    When tags is omitted, unique tag names are automatically collected from all registered endpoints and emitted as name-only entries — so Swagger UI and Redoc still group operations correctly. Any endpoint tag not present in the explicit list is appended alphabetically.

    const info: OpenApiInfo = {
    title: 'My API',
    version: '2.0.0',
    description: 'Full description of my API.',
    termsOfService: 'https://example.com/tos',
    contact: { name: 'Support', email: 'support@example.com' },
    license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' }
    };

    Both path styles are supported:

    // Colon style — converted to {id}
    endpoint.get('/api/users/:id');

    // ParseStringSchemaBuilder — type-safe, with schema
    import { route } from '@cleverbrush/server';
    import { object, number } from '@cleverbrush/schema';

    endpoint.get(route(object({ id: number().coerce() }), $t => $t`/api/users/${t => t.id}`));
    // produces path "/api/users/{id}" with schema { type: 'number' }

    generateAsyncApiSpec() and serveAsyncApi() convert @cleverbrush/server WebSocket subscription registrations into an AsyncAPI 3.0 document — no annotations required.

    import { serveAsyncApi } from '@cleverbrush/server-openapi';

    server.use(serveAsyncApi({
    server,
    info: { title: 'My API', version: '1.0.0' },
    // Optional: document the WebSocket servers
    servers: {
    production: { host: 'api.example.com', protocol: 'wss' },
    local: { host: 'localhost:3000', protocol: 'ws' },
    },
    path: '/asyncapi.json', // default
    }));
    // GET /asyncapi.json → AsyncAPI 3.0 document (lazily generated, cached)
    import { generateAsyncApiSpec } from '@cleverbrush/server-openapi';

    const spec = generateAsyncApiSpec({
    subscriptions: server.getSubscriptionRegistrations(),
    info: { title: 'My API', version: '1.0.0' },
    servers: {
    production: { host: 'api.example.com', protocol: 'wss' },
    },
    });

    // write to file, validate, upload to AsyncAPI Studio, etc.
    await fs.writeFile('asyncapi.json', JSON.stringify(spec, null, 2));

    Each subscription endpoint becomes:

    • A channel (keyed by the subscription's operationId or a sanitised form of its path) with an address containing the WebSocket URL path.
    • A send operation if the endpoint has an outgoing schema (server → client events).
    • A receive operation if the endpoint has an incoming schema (client → server messages).

    Named schemas (registered via .schemaName()) are collected into components.schemas and referenced via $ref pointers in the channel messages.

    Field Type Default Description
    subscriptions readonly SubscriptionRegistration[] From server.getSubscriptionRegistrations()
    info AsyncApiInfo { title, version, description?, ... }
    servers Record<string, AsyncApiServerEntry> {} Named WebSocket server entries
    defaultHost string Fallback host when servers is empty

    BSD-3-Clause — see LICENSE.

    Classes

    SchemaRegistry

    Interfaces

    AsyncApiInfo
    AsyncApiOptions
    AsyncApiServer_ServerLike
    AsyncApiServerEntry
    OpenApiEndpointOptions
    OpenApiInfo
    OpenApiOptions
    OpenApiSecurityScheme
    OpenApiServer
    OpenApiServer_ServerLike
    OpenApiTag
    PathParameterInfo
    ResolvedPath
    ServeAsyncApiOptions
    ServeOpenApiOptions

    Type Aliases

    AsyncApiDocument
    OpenApiDocument

    Functions

    convertSchema
    createOpenApiEndpoint
    generateAsyncApiSpec
    generateOpenApiSpec
    mapOperationSecurity
    mapSecuritySchemes
    resolvePath
    serveAsyncApi
    serveOpenApi
    walkSchemas
    writeOpenApiSpec