serverstruct
Version:
Type safe and modular servers with H3
443 lines (442 loc) • 18.3 kB
text/typescript
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 };