UNPKG

serverstruct

Version:

Type safe and modular servers with H3

443 lines (442 loc) 18.3 kB
import { AnyApiReferenceConfiguration } from "./openapi.scalar.cjs"; import { EventHandlerResponse, H3, H3Event, H3Plugin, RouteOptions } from "h3"; import { Box, Constructor } from "getbox"; import { ZodType, output } from "zod"; import { CreateDocumentOptions, CreateDocumentOptions as CreateDocumentOptions$1, ZodOpenApiMediaTypeObject, ZodOpenApiMetadata, ZodOpenApiMetadata as ZodOpenApiMetadata$1, ZodOpenApiObject, ZodOpenApiObject as ZodOpenApiObject$1, ZodOpenApiOperationObject, ZodOpenApiPathsObject, ZodOpenApiRequestBodyObject, ZodOpenApiResponseObject, createDocument } from "zod-openapi"; //#region src/openapi.d.ts /** Infer the Zod output type from `requestParams.path`. */ type InferParams<T> = T extends { requestParams: { path: infer S; }; } ? output<S> : Record<string, string>; /** Infer the Zod output type from `requestParams.query`. */ type InferQuery<T> = T extends { requestParams: { query: infer S; }; } ? output<S> : Record<string, string>; /** Infer the Zod output type from `requestBody.content["application/json"].schema`. */ type InferBody<T> = T extends { requestBody: { content: { "application/json": { schema: infer S; }; }; }; } ? output<S> : unknown; /** Resolve a response object from `responses` by numeric status code, handling both numeric and string keys. */ type LookupResponseByStatus<R$1, Status extends number> = Status extends keyof R$1 ? R$1[Status] : `${Status}` extends keyof R$1 ? R$1[`${Status}`] : never; /** Infer the Zod output type from `responses[status].content["application/json"].schema`. */ type InferResponse<T, Status extends number> = T extends { responses: infer R; } ? LookupResponseByStatus<R, Status> extends { content: { "application/json": { schema: infer S; }; }; } ? output<S> : unknown : unknown; /** Infer the Zod output type from `responses[status].headers`. Falls back to `Record<string, string>` when no headers schema is defined. */ type InferResponseHeaders<T, Status extends number> = T extends { responses: infer R; } ? LookupResponseByStatus<R, Status> extends { headers: infer H; } ? output<H> : Record<string, string> : Record<string, string>; /** Extract the raw Zod schema from `requestBody.content["application/json"].schema`, or `undefined` if absent. */ type ExtractBodySchema<T> = T extends { requestBody: { content: { "application/json": { schema: infer S; }; }; }; } ? S extends { _zod: any; } ? S : undefined : undefined; /** Extract the raw Zod schema from `requestParams.path`, or `undefined` if absent. */ type ExtractParamsSchema<T> = T extends { requestParams: { path: infer S; }; } ? S extends { _zod: any; } ? S : undefined : undefined; /** Extract the raw Zod schema from `requestParams.query`, or `undefined` if absent. */ type ExtractQuerySchema<T> = T extends { requestParams: { query: infer S; }; } ? S extends { _zod: any; } ? S : undefined : undefined; /** Extract the raw Zod schema from `requestParams.header`, or `undefined` if absent. */ type ExtractHeadersSchema<T> = T extends { requestParams: { header: infer S; }; } ? S extends { _zod: any; } ? S : undefined : undefined; /** Extract the raw Zod schema from `requestParams.cookie`, or `undefined` if absent. */ type ExtractCookiesSchema<T> = T extends { requestParams: { cookie: infer S; }; } ? S extends { _zod: any; } ? S : undefined : undefined; /** Extract numeric status codes from `responses`, normalizing string keys like `"200"` to `200`. */ type ResponseStatusKeys<T> = T extends { responses: infer R; } ? keyof R extends infer K ? K extends number ? K : K extends `${infer N extends number}` ? N : never : never : never; /** * Typed context returned from operation registration. * * Provides access to raw Zod schemas for manual validation and * convenience methods for extracting validated request data. * * - `schemas` — raw Zod schemas for use with H3 validation utilities (e.g. `getValidatedRouterParams`) * - `schemas.params` — path parameters schema * - `schemas.query` — query parameters schema * - `schemas.headers` — request headers schema * - `schemas.cookies` — cookies schema * - `schemas.body` — request body schema * - `params()` — validates and returns route parameters * - `query()` — validates and returns query string parameters * - `body()` — validates and returns the JSON request body * - `reply()` — sets the response status, optional headers, and returns typed response data * - `validReply()` — validates the response data and headers, then sets the response status and returns typed response data */ type RouterContext<T extends ZodOpenApiOperationObject = ZodOpenApiOperationObject> = { schemas: { params: ExtractParamsSchema<T>; query: ExtractQuerySchema<T>; headers: ExtractHeadersSchema<T>; cookies: ExtractCookiesSchema<T>; body: ExtractBodySchema<T>; }; /** * Validates and returns route parameters. * Uses `getValidatedRouterParams()` from H3 when schema is present, * otherwise uses `getRouterParams()`. */ params(event: H3Event): Promise<InferParams<T>>; /** * Validates and returns query string parameters. * Uses `getValidatedQuery()` from H3 when schema is present, * otherwise uses `getQuery()`. */ query(event: H3Event): Promise<InferQuery<T>>; /** * Validates and returns the request body. * Uses `readValidatedBody()` from H3 when schema is present, * otherwise uses `readBody()`. * Reads request body and tries to parse using JSON.parse or URLSearchParams. */ body(event: H3Event): Promise<InferBody<T>>; /** * Sets the response status and optional headers, then returns the typed response data. * Does not perform runtime validation on the response data. */ reply<S$1 extends ResponseStatusKeys<T>>(event: H3Event, status: S$1, data: InferResponse<T, S$1>, headers?: InferResponseHeaders<T, S$1>): InferResponse<T, S$1>; /** * Validates the response data, sets the response status and optional headers, then returns the typed response data. * Throws an error if validation fails. */ validReply<S$1 extends ResponseStatusKeys<T>>(event: H3Event, status: S$1, data: InferResponse<T, S$1>, headers?: InferResponseHeaders<T, S$1>): InferResponse<T, S$1>; }; declare const HTTP_METHODS: readonly ["get", "post", "put", "delete", "patch"]; type HttpMethod = (typeof HTTP_METHODS)[number]; /** * Collects OpenAPI operation definitions for document generation. * * Register operations by HTTP method and path. The accumulated `paths` * object can be passed to `createDocument()` to generate the OpenAPI spec. * * Each registration returns a typed {@link RouterContext} for use in route handlers. * * @example * ```ts * const paths = new OpenApiPaths(); * * const getPost = paths.get("/posts/{id}", { ... }); * * // Generate OpenAPI document * createDocument({ openapi: "3.1.0", info: { ... }, paths: paths.paths }); * ``` */ declare class OpenApiPaths { /** Accumulated OpenAPI paths object. */ paths: ZodOpenApiPathsObject; /** Register an operation for the GET method. */ get<T extends ZodOpenApiOperationObject>(path: string, operation: T): RouterContext<T>; /** Register an operation for the POST method. */ post<T extends ZodOpenApiOperationObject>(path: string, operation: T): RouterContext<T>; /** Register an operation for the PUT method. */ put<T extends ZodOpenApiOperationObject>(path: string, operation: T): RouterContext<T>; /** Register an operation for the DELETE method. */ delete<T extends ZodOpenApiOperationObject>(path: string, operation: T): RouterContext<T>; /** Register an operation for the PATCH method. */ patch<T extends ZodOpenApiOperationObject>(path: string, operation: T): RouterContext<T>; /** Register an operation for all standard HTTP methods (get, post, put, delete, patch). */ all<T extends ZodOpenApiOperationObject>(path: string, operation: T): RouterContext<T>; /** Register an operation for specific HTTP methods. */ on<T extends ZodOpenApiOperationObject>(methods: readonly HttpMethod[], path: string, operation: T): RouterContext<T>; /** * Mount all paths from `sub` with a base prefix. * * Existing entries on the same path and method are not overwritten. * * @example * ```ts * const subPaths = new OpenApiPaths(); * * subPaths.get("/", { operationId: "getUsers", responses: {} }); * * const basePaths = new OpenApiPaths(); * basePaths.mount("/users", subPaths); * ``` */ mount(base: string, sub: OpenApiPaths): void; } /** * Combines OpenAPI path registration with H3 route registration. * * Each method registers the operation in {@link OpenApiPaths} (converting the * H3 path syntax to OpenAPI format) and simultaneously registers the route * handler on the H3 app. The handler receives the typed {@link RouterContext}. * * @example * ```ts * const router = useRouter(app); * * router.get("/posts/:id", { * operationId: "getPost", * requestBody: jsonRequest(inputSchema), * responses: { * 200: jsonResponse(outputSchema, { description: "Success" }), * }, * }, async (event, ctx) => { * const body = await ctx.body(event); * return ctx.reply(event, 200, { message: "ok" }); * }); * ``` */ declare class OpenApiRouter { protected _app: H3; protected _paths: OpenApiPaths; private static key; /** * Returns the existing router for `app`, or creates and attaches a new one. * Multiple calls on the same app return the same instance. */ static from(app: H3): OpenApiRouter; private constructor(); /** Register a route and operation for the GET method. */ get<T extends ZodOpenApiOperationObject>(path: string, operation: T, handler: (event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse, opts?: RouteOptions): this; /** Register a route and operation for the POST method. */ post<T extends ZodOpenApiOperationObject>(path: string, operation: T, handler: (event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse, opts?: RouteOptions): this; /** Register a route and operation for the PUT method. */ put<T extends ZodOpenApiOperationObject>(path: string, operation: T, handler: (event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse, opts?: RouteOptions): this; /** Register a route and operation for the DELETE method. */ delete<T extends ZodOpenApiOperationObject>(path: string, operation: T, handler: (event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse, opts?: RouteOptions): this; /** Register a route and operation for the PATCH method. */ patch<T extends ZodOpenApiOperationObject>(path: string, operation: T, handler: (event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse, opts?: RouteOptions): this; /** Register a route and operation for all standard HTTP methods. */ all<T extends ZodOpenApiOperationObject>(path: string, operation: T, handler: (event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse, opts?: RouteOptions): this; /** Register a route and operation for specific HTTP methods. */ on<T extends ZodOpenApiOperationObject>(methods: readonly HttpMethod[], path: string, operation: T, handler: (event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse, opts?: RouteOptions): this; /** * Registers routes and operations for standalone {@link Route} definitions. */ route(...routes: Route<any>[]): this; /** * Mounts a sub-app and adds all paths with a base prefix. * * When mounting a sub-app, all routes will be added with base prefix and global middleware will be added as one prefixed middleware. * * **Note:** Sub-app options and global hooks are not inherited when mounted consider setting them in the main app directly. */ mount(base: string, sub: H3): this; /** * Mounts sub-apps resolved from a {@link Box} under their respective base prefixes. * * When mounting a sub-app, all routes will be added with base prefix and global middleware will be added as one prefixed middleware. * * **Note:** Sub-app options and global hooks are not inherited when mounted consider setting them in the main app directly. */ mount(box: Box, routes: Record<string, Constructor<H3>>): this; /** Returns the accumulated OpenAPI paths object. */ paths(): ZodOpenApiPathsObject; /** * Mounts a handler at `path` that serves the OpenAPI document. * Also mounts a Scalar API reference UI at `{path}/reference` by default. * Pass `reference: false` to disable, or provide options to configure it. */ document(path: string, options: RouterDocumentOptions): this; } /** API document options. */ interface RouterDocumentOptions extends Omit<ZodOpenApiObject$1, "paths"> { options?: CreateDocumentOptions$1; reference?: false | RouterReferenceOptions; } /** API reference options. */ interface RouterReferenceOptions { /** Path to mount the Scalar UI. Defaults to `{documentPath}/reference`. */ path?: string; /** Scalar configuration options (excluding `url`, which is set automatically). */ configuration?: Omit<AnyApiReferenceConfiguration, "url">; /** Page title. Defaults to "Scalar API Reference". */ pageTitle?: string; /** CDN URL for the standalone bundle. Defaults to jsDelivr. */ cdn?: string; /** Custom CSS theme for the Scalar UI. */ customTheme?: string; } /** * Creates an {@link OpenApiRouter} that combines H3 route registration with OpenAPI path collection. * * Multiple calls on the same app return the same instance. * * @param app - H3 application instance. * @returns An {@link OpenApiRouter} instance. * * @example * ```ts * const router = useRouter(app); * * router.get("/posts/:id", { * operationId: "getPost", * responses: { 200: jsonResponse(postSchema, { description: "Success" }) }, * }, async (event, ctx) => { * const { id } = await ctx.params(event); * return ctx.reply(event, 200, { id }); * }); * ``` */ declare function useRouter(app: H3): OpenApiRouter; /** * Creates a Route constructor. * * `setup` is called once with the Box to resolve dependencies. Return a handler function * directly, or an object with a `handler` and other route options (e.g. `meta`, `middleware`). * * @param options.setup - Returns the handler or `{ handler, ...RouteOptions }`. * @returns A Constructor that produces a {@link Route}. Not cached by Box. * * @example * ```ts * const getPost = route({ * method: "get", * path: "/posts/:id", * operation: { * operationId: "getPost", * responses: { 200: jsonResponse(postSchema, { description: "Success" }) }, * }, * setup(box) { * const db = box.get(Database); * return async (event, ctx) => { * const { id } = await ctx.params(event); * return ctx.reply(event, 200, await db.getPost(id)); * }; * }, * }); * * const getPostRoute = box.get(getPost); * * const router = useRouter(app); * router.route(getPostRoute); * ``` */ declare function route<T extends ZodOpenApiOperationObject>(options: { method: HttpMethod | readonly HttpMethod[]; path: string; operation: T; setup: (box: Box) => ((event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse) | PrettyMerge<RouteOptions, { handler: (event: H3Event, ctx: RouterContext<T>) => EventHandlerResponse; }>; }): Constructor<Route<T>>; /** * A route plugin produced by {@link route}. * * Call with an {@link OpenApiPaths} instance to get an {@link H3Plugin} * that registers the operation and mounts the handler on the app. */ type Route<_T extends ZodOpenApiOperationObject> = (paths: OpenApiPaths) => H3Plugin; type Pretty<T> = { [K in keyof T]: T[K] } & {}; type Merge<T, U> = Omit<T, keyof U> & U; type PrettyOmit<T, U extends keyof any> = Pretty<Omit<T, U>>; type PrettyMerge<T, U> = Pretty<Merge<T, U>>; /** Builder for OpenAPI metadata passed to `.meta()` on Zod schemas. */ declare const metadata: (meta: ZodOpenApiMetadata$1) => ZodOpenApiMetadata$1; /** * Build a typed `requestBody` object with `application/json` content. * * Additional media type options (e.g. `example`) can be passed via `opts.content`. * * @example * ```ts * jsonRequest(inputSchema) * jsonRequest(inputSchema, { description: "Create a post", content: { example: { title: "Hello" } } }) * ``` */ declare function jsonRequest<S$1 extends { _zod: any; }, O extends PrettyMerge<ZodOpenApiRequestBodyObject, { content?: PrettyOmit<ZodOpenApiMediaTypeObject, "schema">; }>>(schema: S$1, opts?: O): PrettyMerge<ZodOpenApiRequestBodyObject, { content: { "application/json": PrettyMerge<{ schema: S$1; }, O["content"]>; }; }>; /** * Build a typed response object with `application/json` content. * * Additional media type options (e.g. `example`) can be passed via `opts.content`. * * @example * ```ts * jsonResponse(outputSchema, { description: "Success" }) * jsonResponse(outputSchema, { * description: "Success", * headers: z.object({ "x-request-id": z.string() }), * }) * ``` */ declare function jsonResponse<S$1 extends { _zod: any; }, H$1 extends { _zod: any; } | undefined, O extends PrettyMerge<ZodOpenApiResponseObject, { content?: PrettyOmit<ZodOpenApiMediaTypeObject, "schema">; headers?: H$1; }>>(schema: S$1, opts: O): PrettyMerge<ZodOpenApiResponseObject, { content: { "application/json": PrettyMerge<{ schema: S$1; }, O["content"]>; }; headers: O["headers"]; }>; /** * Creates a typed schemas object for grouping route schemas together. * * The common keys are `params`, `query`, `headers`, `cookies`, `body`, and * `response`. Other schema properties can also be added. */ declare function schemas<T extends { params?: ZodType; query?: ZodType; headers?: ZodType; cookies?: ZodType; body?: ZodType; response?: ZodType; [k: string]: ZodType | undefined; }>(s: T): T; //#endregion export { type CreateDocumentOptions, OpenApiPaths, OpenApiRouter, Route, RouterContext, RouterDocumentOptions, RouterReferenceOptions, type ZodOpenApiMetadata, type ZodOpenApiObject, createDocument, jsonRequest, jsonResponse, metadata, route, schemas, useRouter };