Initial Commit
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# route-factory
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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`/`DELETE` shorthands.
|
||||
- `StandardResponses` widened to cover the full SuaveplanError status range.
|
||||
- `ErrorResponseSchema` carrying the `cause` field from `SuaveplanError.toJSON()`.
|
||||
- `successResponse(schema)`, `paginatedResponse(itemSchema)`, `errorResponse(description)` helpers.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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 ErrorResponse`
|
||||
- `function 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 at `ErrorResponseSchema`
|
||||
- `type TypedRouteConfig<TParams, TQuery, TBody, TResponse>`
|
||||
- `class RouteBuilder<TParams, TQuery, TBody, TResponse>`
|
||||
- `function route(method, path): RouteBuilder`
|
||||
- `const 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 `Error`s, 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.
|
||||
Reference in New Issue
Block a user