From 4107b7a92e943b1815a11d7cbba72b1e57798037 Mon Sep 17 00:00:00 2001 From: Andrew Zolotukhin Date: Tue, 14 Apr 2026 15:13:37 +0000 Subject: [PATCH] Add HTTP status code descriptions and enhance response handling in OpenAPI spec generation --- .../server-openapi/src/generateOpenApiSpec.ts | 111 ++++- libs/server/src/ActionResult.ts | 112 ++++- libs/server/src/Endpoint.ts | 427 +++++++++++++++--- libs/server/src/Server.ts | 4 +- libs/server/src/index.ts | 2 + 5 files changed, 562 insertions(+), 94 deletions(-) diff --git a/libs/server-openapi/src/generateOpenApiSpec.ts b/libs/server-openapi/src/generateOpenApiSpec.ts index 9e9d544..9ef3953 100644 --- a/libs/server-openapi/src/generateOpenApiSpec.ts +++ b/libs/server-openapi/src/generateOpenApiSpec.ts @@ -103,34 +103,112 @@ function buildRequestBody( return body; } +// Default descriptions for common HTTP status codes +const HTTP_STATUS_DESCRIPTIONS: Record = { + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 204: 'No Content', + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 409: 'Conflict', + 422: 'Unprocessable Entity', + 500: 'Internal Server Error' +}; + +// Minimal inline schema for ProblemDetails error responses +const PROBLEM_DETAILS_SCHEMA = { + type: 'object', + properties: { + status: { type: 'integer' }, + title: { type: 'string' }, + detail: { type: 'string' } + } +}; + function buildResponses( - responseSchema: SchemaBuilder | null, + meta: EndpointMetadata, method: string ): Record { - if (responseSchema) { - const jsonSchema = convertSchema(responseSchema); - const respInfo = responseSchema.introspect() as any; + const result: Record = {}; + + // Multi-code path — .responses() was called + if (meta.responsesSchemas) { + for (const [codeStr, schema] of Object.entries(meta.responsesSchemas)) { + const code = Number(codeStr); + const desc = + HTTP_STATUS_DESCRIPTIONS[code] ?? `Response ${codeStr}`; + if (schema) { + const jsonSchema = convertSchema(schema); + const respInfo = schema.introspect() as any; + const customDesc = + typeof respInfo.description === 'string' && + respInfo.description !== '' + ? respInfo.description + : desc; + result[codeStr] = { + description: customDesc, + content: { 'application/json': { schema: jsonSchema } } + }; + } else { + result[codeStr] = { description: desc }; + } + } + } else if (meta.responseSchema) { + // Legacy single-code path — .returns() was called + const jsonSchema = convertSchema(meta.responseSchema); + const respInfo = meta.responseSchema.introspect() as any; const desc = typeof respInfo.description === 'string' && respInfo.description !== '' ? respInfo.description : 'Successful response'; - return { - '200': { - description: desc, - content: { - 'application/json': { schema: jsonSchema } - } + result['200'] = { + description: desc, + content: { 'application/json': { schema: jsonSchema } } + }; + } else if (method === 'DELETE' || method === 'HEAD') { + result['204'] = { description: 'No content' }; + } else { + result['200'] = { description: 'Successful response' }; + } + + // Auto-add framework-generated error responses + if (meta.bodySchema && !result['422']) { + result['422'] = { + description: 'Validation error', + content: { + 'application/problem+json': { schema: PROBLEM_DETAILS_SCHEMA } } }; } - // No response schema — use 204 for methods that typically don't return content - if (method === 'DELETE' || method === 'HEAD') { - return { '204': { description: 'No content' } }; + if (meta.authRoles !== null) { + if (!result['401']) { + result['401'] = { + description: 'Unauthorized', + content: { + 'application/problem+json': { + schema: PROBLEM_DETAILS_SCHEMA + } + } + }; + } + if (!result['403']) { + result['403'] = { + description: 'Forbidden', + content: { + 'application/problem+json': { + schema: PROBLEM_DETAILS_SCHEMA + } + } + }; + } } - return { '200': { description: 'Successful response' } }; + return result; } function buildOperation( @@ -217,10 +295,7 @@ function buildOperation( } // Responses - operation['responses'] = buildResponses( - meta.responseSchema, - meta.method.toUpperCase() - ); + operation['responses'] = buildResponses(meta, meta.method.toUpperCase()); // Security const security = mapOperationSecurity(authRoles(meta), securitySchemeNames); diff --git a/libs/server/src/ActionResult.ts b/libs/server/src/ActionResult.ts index f237152..a3c00c2 100644 --- a/libs/server/src/ActionResult.ts +++ b/libs/server/src/ActionResult.ts @@ -37,19 +37,30 @@ export abstract class ActionResult { // ----------------------------------------------------------------------- /** 200 OK — serializes value using content negotiation. */ - static ok(body: unknown, headers?: Record): JsonResult { - return new JsonResult(body, 200, headers); + static ok( + body: T, + headers?: Record + ): JsonResult<200, T> { + return new JsonResult(body, 200, headers) as JsonResult<200, T>; } /** 201 Created — serializes value using content negotiation. */ - static created( - body: unknown, + static created( + body: T, location?: string, headers?: Record - ): JsonResult { + ): JsonResult<201, T> { const h: Record = { ...headers }; if (location) h['location'] = location; - return new JsonResult(body, 201, h); + return new JsonResult(body, 201, h) as JsonResult<201, T>; + } + + /** 202 Accepted — serializes value using content negotiation. */ + static accepted( + body: T, + headers?: Record + ): JsonResult<202, T> { + return new JsonResult(body, 202, headers) as JsonResult<202, T>; } /** 204 No Content. */ @@ -57,15 +68,65 @@ export abstract class ActionResult { return new NoContentResult(); } + /** 400 Bad Request — serializes value as JSON. */ + static badRequest( + body: T, + headers?: Record + ): JsonResult<400, T> { + return new JsonResult(body, 400, headers) as JsonResult<400, T>; + } + + /** 401 Unauthorized — serializes value as JSON. */ + static unauthorized( + body: T, + headers?: Record + ): JsonResult<401, T> { + return new JsonResult(body, 401, headers) as JsonResult<401, T>; + } + + /** 403 Forbidden — serializes value as JSON. */ + static forbidden( + body: T, + headers?: Record + ): JsonResult<403, T> { + return new JsonResult(body, 403, headers) as JsonResult<403, T>; + } + + /** 404 Not Found — serializes value as JSON. */ + static notFound( + body: T, + headers?: Record + ): JsonResult<404, T> { + return new JsonResult(body, 404, headers) as JsonResult<404, T>; + } + + /** 409 Conflict — serializes value as JSON. */ + static conflict( + body: T, + headers?: Record + ): JsonResult<409, T> { + return new JsonResult(body, 409, headers) as JsonResult<409, T>; + } + /** Temporary (302) or permanent (301) redirect. */ static redirect(url: string, permanent = false): RedirectResult { return new RedirectResult(url, permanent); } - /** Explicit JSON response — always uses application/json regardless of Accept. */ + /** + * Explicit JSON response with a specific status code. + * Use the named factories (`ok`, `notFound`, etc.) for common codes. + * This overload is an escape hatch for uncommon status codes. + */ + static json(body: T): JsonResult<200, T>; + static json( + body: T, + status: S, + headers?: Record + ): JsonResult; static json( body: unknown, - status = 200, + status: number = 200, headers?: Record ): JsonResult { return new JsonResult(body, status, headers); @@ -99,11 +160,11 @@ export abstract class ActionResult { } /** Bare status code with no body. */ - static status( - status: number, + static status( + status: S, headers?: Record - ): StatusCodeResult { - return new StatusCodeResult(status, headers); + ): StatusCodeResult { + return new StatusCodeResult(status, headers) as StatusCodeResult; } } @@ -119,15 +180,22 @@ export abstract class ActionResult { * `ActionResult.ok()` and `ActionResult.created()` produce a {@link JsonResult} * that goes through content negotiation instead. */ -export class JsonResult extends ActionResult { - readonly body: unknown; - readonly status: number; +export class JsonResult< + TStatus extends number = number, + TBody = unknown +> extends ActionResult { + readonly body: TBody; + readonly status: TStatus; readonly headers: Record; - constructor(body: unknown, status = 200, headers?: Record) { + constructor( + body: TBody, + status: TStatus | number = 200, + headers?: Record + ) { super(); this.body = body; - this.status = status; + this.status = status as TStatus; this.headers = headers ?? {}; } @@ -270,13 +338,15 @@ export class StreamResult extends ActionResult { * Responds with a bare HTTP status code and no body. * Created by `ActionResult.status()`. */ -export class StatusCodeResult extends ActionResult { - readonly status: number; +export class StatusCodeResult< + TStatus extends number = number +> extends ActionResult { + readonly status: TStatus; readonly headers: Record; - constructor(status: number, headers?: Record) { + constructor(status: TStatus | number, headers?: Record) { super(); - this.status = status; + this.status = status as TStatus; this.headers = headers ?? {}; } diff --git a/libs/server/src/Endpoint.ts b/libs/server/src/Endpoint.ts index e00aca3..4a7758d 100644 --- a/libs/server/src/Endpoint.ts +++ b/libs/server/src/Endpoint.ts @@ -4,7 +4,16 @@ import type { ParseStringSchemaBuilder, SchemaBuilder } from '@cleverbrush/schema'; -import type { ActionResult } from './ActionResult.js'; +import type { + ActionResult, + ContentResult, + FileResult, + JsonResult, + NoContentResult, + RedirectResult, + StatusCodeResult, + StreamResult +} from './ActionResult.js'; import type { RequestContext } from './RequestContext.js'; // --------------------------------------------------------------------------- @@ -42,6 +51,7 @@ export type ActionContext = any, infer TPrincipal, any, + any, any > ? Simplify< @@ -72,6 +82,7 @@ export type ServiceSchemas = infer TServices, any, any, + any, any > ? TServices @@ -86,11 +97,62 @@ type ResponseType = any, any, any, - infer TResponse + infer TResponse, + any > ? TResponse : any; +/** + * Extracts the `TResponses` map from an `EndpointBuilder` type. + * `TResponses` is a `Record` inferred from `.responses()`. + */ +export type ResponsesOf = + E extends EndpointBuilder< + any, + any, + any, + any, + any, + any, + any, + any, + infer TResponses + > + ? TResponses + : never; + +type HasResponses = keyof ResponsesOf extends never ? false : true; + +/** + * The union of permitted return values for a handler whose endpoint + * declared `.responses()`. Each member corresponds to one declared code: + * + * - `null` schema (e.g. 204) → `NoContentResult` for 204, `StatusCodeResult` otherwise + * - Non-null schema for code 200 → also allows a plain object (treated as 200 by the server) + * - Non-null schema for other codes → `JsonResult` + * + * `FileResult`, `StreamResult`, `ContentResult`, and `RedirectResult` are always + * permitted as an escape hatch for non-JSON responses. + */ +export type AllowedResponseReturn> = + | { + [K in keyof TResponses & number]: TResponses[K] extends null + ? K extends 204 + ? NoContentResult + : StatusCodeResult + : JsonResult; + }[keyof TResponses & number] + | (200 extends keyof TResponses + ? TResponses[200] extends null + ? never + : TResponses[200] + : never) + | FileResult + | StreamResult + | ContentResult + | RedirectResult; + // --------------------------------------------------------------------------- // Handler — the action function type, inferred from an endpoint // --------------------------------------------------------------------------- @@ -101,21 +163,20 @@ type ResponseType = * When the endpoint has injected services, the handler receives a second * `services` argument with all resolved service instances. */ +type HandlerReturn = + HasResponses extends true + ? AllowedResponseReturn> + : ResponseType | ActionResult; + export type Handler = HasKeys> extends true ? ( arg: ActionContext, services: Simplify>> - ) => - | ResponseType - | ActionResult - | Promise | ActionResult> + ) => HandlerReturn | Promise> : ( arg: ActionContext - ) => - | ResponseType - | ActionResult - | Promise | ActionResult>; + ) => HandlerReturn | Promise>; // --------------------------------------------------------------------------- // EndpointBuilder — immutable builder for endpoint definitions @@ -168,6 +229,16 @@ export interface EndpointMetadata { readonly operationId: string | null; readonly deprecated: boolean; readonly responseSchema: SchemaBuilder | null; + /** + * Per-status-code response schemas declared via `.responses()`. + * When non-null, takes precedence over `responseSchema` for OpenAPI generation + * and constrains the handler return type to the declared codes. + * A `null` schema value means the response has no body (e.g. 204). + */ + readonly responsesSchemas: Record< + number, + SchemaBuilder | null + > | null; } /** @@ -187,6 +258,19 @@ export interface EndpointMetadata { * .summary('Get a user by ID'); * ``` */ +/** + * Infers the per-code body type map from a `.responses()` schema map. + * Each schema maps to its `InferType`; a `null` schema maps to `null` + * (meaning the response has no body). + */ +type InferResponsesMap< + T extends Record | null> +> = { + [K in keyof T]: T[K] extends SchemaBuilder + ? InferType + : null; +}; + export class EndpointBuilder< TParams = {}, TBody = undefined, @@ -195,7 +279,8 @@ export class EndpointBuilder< TServices = {}, TPrincipal = undefined, TRoles extends string = string, - TResponse = any + TResponse = any, + TResponses extends Record = {} > { readonly #method: string; readonly #basePath: string; @@ -230,6 +315,10 @@ export class EndpointBuilder< readonly #operationId: string | null; readonly #deprecated: boolean; readonly #responseSchema: SchemaBuilder | null; + readonly #responsesSchemas: Record< + number, + SchemaBuilder | null + > | null; constructor( method: string, @@ -264,7 +353,11 @@ export class EndpointBuilder< tags: readonly string[] = [], operationId: string | null = null, deprecated: boolean = false, - responseSchema: SchemaBuilder | null = null + responseSchema: SchemaBuilder | null = null, + responsesSchemas: Record< + number, + SchemaBuilder | null + > | null = null ) { this.#method = method; this.#basePath = basePath; @@ -280,9 +373,9 @@ export class EndpointBuilder< this.#operationId = operationId; this.#deprecated = deprecated; this.#responseSchema = responseSchema; + this.#responsesSchemas = responsesSchemas; } - /** Define the request body schema. Validation failures return 422 Problem Details. */ /** Define the request body schema. Validation failures return 422 Problem Details. */ body>( schema: TSchema @@ -294,7 +387,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -310,7 +404,8 @@ export class EndpointBuilder< this.#tags, this.#operationId, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -327,7 +422,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -343,7 +439,8 @@ export class EndpointBuilder< this.#tags, this.#operationId, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -360,7 +457,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -376,7 +474,8 @@ export class EndpointBuilder< this.#tags, this.#operationId, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -393,7 +492,8 @@ export class EndpointBuilder< TSchemas, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -409,7 +509,8 @@ export class EndpointBuilder< this.#tags, this.#operationId, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -433,7 +534,8 @@ export class EndpointBuilder< TServices, InferType, TRoles, - TResponse + TResponse, + TResponses >; authorize( ...roles: TRoles[] @@ -445,7 +547,8 @@ export class EndpointBuilder< TServices, unknown, TRoles, - TResponse + TResponse, + TResponses >; authorize( ...args: unknown[] @@ -457,7 +560,8 @@ export class EndpointBuilder< TServices, any, TRoles, - TResponse + TResponse, + TResponses > { let roles: string[]; if ( @@ -489,7 +593,8 @@ export class EndpointBuilder< this.#tags, this.#operationId, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -508,7 +613,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - T + T, + TResponses >; returns>( schema: TSchema @@ -520,11 +626,12 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - InferType + InferType, + TResponses >; returns( _schema?: unknown - ): EndpointBuilder { + ): EndpointBuilder { const schema = _schema != null && typeof _schema === 'object' && @@ -545,7 +652,67 @@ export class EndpointBuilder< this.#tags, this.#operationId, this.#deprecated, - schema ?? this.#responseSchema + schema ?? this.#responseSchema, + this.#responsesSchemas + ); + } + + /** + * Declare per-status-code response schemas for OpenAPI generation and + * handler return-type enforcement. + * + * Pass `null` as the schema for body-less codes (e.g. 204). + * + * @example + * ```ts + * const ep = endpoint + * .get('/api/todos/:id') + * .responses({ + * 200: object({ id: number(), title: string() }), + * 404: object({ message: string() }), + * }); + * + * const handler: Handler = ({ params }) => { + * const todo = todos.get(params.id); + * if (!todo) return ActionResult.notFound({ message: 'Not found' }); + * return todo; // plain object → 200 + * }; + * ``` + */ + responses< + const T extends Record< + number, + SchemaBuilder | null + > + >( + map: T + ): EndpointBuilder< + TParams, + TBody, + TQuery, + THeaders, + TServices, + TPrincipal, + TRoles, + TResponse, + InferResponsesMap + > { + return new EndpointBuilder( + this.#method, + this.#basePath, + this.#pathTemplate, + this.#bodySchema, + this.#querySchema, + this.#headerSchema, + this.#serviceSchemas, + this.#authRoles, + this.#summary, + this.#description, + this.#tags, + this.#operationId, + this.#deprecated, + this.#responseSchema, + map ); } @@ -560,7 +727,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -576,7 +744,8 @@ export class EndpointBuilder< this.#tags, this.#operationId, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -591,7 +760,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -607,7 +777,8 @@ export class EndpointBuilder< this.#tags, this.#operationId, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -622,7 +793,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -638,7 +810,8 @@ export class EndpointBuilder< tags, this.#operationId, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -653,7 +826,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -669,7 +843,8 @@ export class EndpointBuilder< this.#tags, id, this.#deprecated, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -682,7 +857,8 @@ export class EndpointBuilder< TServices, TPrincipal, TRoles, - TResponse + TResponse, + TResponses > { return new EndpointBuilder( this.#method, @@ -698,7 +874,8 @@ export class EndpointBuilder< this.#tags, this.#operationId, true, - this.#responseSchema + this.#responseSchema, + this.#responsesSchemas ); } @@ -718,7 +895,8 @@ export class EndpointBuilder< tags: this.#tags, operationId: this.#operationId, deprecated: this.#deprecated, - responseSchema: this.#responseSchema + responseSchema: this.#responseSchema, + responsesSchemas: this.#responsesSchemas }; } } @@ -767,6 +945,7 @@ function createEndpoint( meta?.tags ?? [], meta?.operationId ?? null, meta?.deprecated ?? false, + null, null ); } @@ -781,25 +960,95 @@ type ScopedEndpointFactoryMethods< > = { get( pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + TPrincipal, + TRoles, + any, + {} + >; post( pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + TPrincipal, + TRoles, + any, + {} + >; put( pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + TPrincipal, + TRoles, + any, + {} + >; patch( pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + TPrincipal, + TRoles, + any, + {} + >; delete( pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + TPrincipal, + TRoles, + any, + {} + >; head( pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + TPrincipal, + TRoles, + any, + {} + >; options( pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + TPrincipal, + TRoles, + any, + {} + >; }; export type ScopedEndpointFactory = @@ -870,31 +1119,101 @@ type EndpointFactory = { get( basePath: string, pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + undefined, + TRoles, + any, + {} + >; post( basePath: string, pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + undefined, + TRoles, + any, + {} + >; put( basePath: string, pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + undefined, + TRoles, + any, + {} + >; patch( basePath: string, pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + undefined, + TRoles, + any, + {} + >; delete( basePath: string, pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + undefined, + TRoles, + any, + {} + >; head( basePath: string, pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + undefined, + TRoles, + any, + {} + >; options( basePath: string, pathTemplate?: ParseStringSchemaBuilder - ): EndpointBuilder; + ): EndpointBuilder< + TParams, + undefined, + {}, + {}, + {}, + undefined, + TRoles, + any, + {} + >; resource(basePath: string): ScopedEndpointFactory; }; diff --git a/libs/server/src/Server.ts b/libs/server/src/Server.ts index 972fd9c..6ef5b79 100644 --- a/libs/server/src/Server.ts +++ b/libs/server/src/Server.ts @@ -152,7 +152,9 @@ export class ServerBuilder { * @param handler - The typed handler function. * @param options - Optional per-endpoint middleware. */ - handle>( + handle< + E extends EndpointBuilder + >( endpointDef: E, handler: Handler, options?: { middlewares?: Middleware[] } diff --git a/libs/server/src/index.ts b/libs/server/src/index.ts index f2c8893..91aaa86 100644 --- a/libs/server/src/index.ts +++ b/libs/server/src/index.ts @@ -10,12 +10,14 @@ export { } from './ActionResult.js'; export { type ActionContext, + type AllowedResponseReturn, createEndpoints, EndpointBuilder, type EndpointMetadata, type EndpointMetadataDescriptors, endpoint, type Handler, + type ResponsesOf, type ScopedEndpointFactory } from './Endpoint.js'; export {