4.5 KiB
route-factory
(no submodules)
Purpose
Typed route construction utilities for @hono/zod-openapi. The
RouteBuilder is a fluent generic that threads TParams, TQuery,
TBody, and TResponse from .params/.query/.body/.response calls
through to the final .build() output. The resulting TypedRouteConfig
is assignable to RouteConfig, so app.openapi(route, handler) infers
c.req.valid("params" | "query" | "json") to the exact Zod type
without unknown widening.
Also exports the wire-shape ErrorResponseSchema, the wider
StandardResponses (covering every status code a SuaveplanError can
emit — 400/401/403/404/409/422/429/500/503/504), and the
successResponse / paginatedResponse / errorResponse envelope
helpers.
Features
RouteBuilder<TParams, TQuery, TBody, TResponse>fluent generic.route(method, path)factory +GET/POST/PUT/PATCH/DELETEshorthands.StandardResponseswidened to cover the full SuaveplanError status range.ErrorResponseSchemacarrying thecausefield fromSuaveplanError.toJSON().successResponse(schema),paginatedResponse(itemSchema),errorResponse(description)helpers.
Usage: Basic
import z from "zod";
import { GET, successResponse } from "@suaveplan/api-openapi/route-factory";
const ListUsers = GET("/users")
.tags("users")
.operationId("listUsers")
.response(successResponse(z.array(z.object({ id: z.string() }))))
.build();
Usage: Advanced
import { OpenAPIHono } from "@hono/zod-openapi";
import z from "zod";
import {
POST,
paginatedResponse,
successResponse,
} from "@suaveplan/api-openapi/route-factory";
const UserParam = z.object({ id: z.string().uuid() });
const UserCreate = z.object({ email: z.string().email() });
const User = z.object({ id: z.string(), email: z.string() });
const createUser = POST("/users/:id")
.tags("users")
.operationId("createUser")
.deprecated()
.security([{ Bearer: [] }])
.params(UserParam)
.body(UserCreate)
.response(successResponse(User))
.build();
const listUsers = POST("/users/list")
.tags("users")
.response(paginatedResponse(User))
.build();
const app = new OpenAPIHono();
app.openapi(createUser, async (c) => {
const params = c.req.valid("param"); // typed as z.infer<typeof UserParam>
const body = c.req.valid("json"); // typed as z.infer<typeof UserCreate>
return c.json({ success: true, data: { id: params.id, email: body.email } });
});
API Reference
type RouteMethod = "get"|"post"|"put"|"patch"|"delete"interface RouteOptions<TParams, TQuery, TBody, TResponse>const ErrorResponseSchema+type ErrorResponsefunction successResponse<T>(dataSchema: T)function errorResponse(description: string)function paginatedResponse<T>(itemSchema: T)const StandardResponses— 400/401/403/404/409/422/429/500/503/504 each pointing atErrorResponseSchematype TypedRouteConfig<TParams, TQuery, TBody, TResponse>class RouteBuilder<TParams, TQuery, TBody, TResponse>function route(method, path): RouteBuilderconst GET/POST/PUT/PATCH/DELETE
Implementation Notes
TypedRouteConfig uses Omit<RouteConfig, "request"> & { request: … }
so the request property is non-optional. Hono's InputType*
conditional types only match when request is present, so making it
optional silently downgrades inference to unknown.
The builder records state in a private object rather than directly on
this so the four .params/.query/.body/.response overloads can re-cast
the receiver to a new generic instantiation without TypeScript losing
track of the actual stored values.
StandardResponses is widened to cover every status SuaveplanError
emits. Upstream omitted 422 (validation), 429 (rate limit), 503
(service unavailable), and 504 (gateway timeout), forcing every plugin
that wanted to document those responses to redefine them locally — a
common source of doc drift. The widened set is now the single source.
ErrorResponseSchema adds the cause field on top of upstream's
shape. SuaveplanError supports a cause chain, and serialising it gives
clients enough context to diagnose without requesting logs. cause is
typed z.unknown() because the chain may be heterogeneous —
SuaveplanError descendants, native Errors, or serialised payloads.
responses always merges 200: Success first and then spreads
StandardResponses — so consumers can override 200 by passing
.response(…) but cannot accidentally drop one of the standard error
slots.