224 lines
11 KiB
Markdown
224 lines
11 KiB
Markdown
# @suaveplan/api-openapi - OpenAPI 3.1 registry, typed RouteBuilder, schema registry, and document merger for the Suaveplan API. Single source of truth that wires into Hono via OpenAPIRegistry.applyTo(app) — eliminating drift between the introspection registry and the served /openapi.json. Ships StandardResponses spanning the full SuaveplanError statusCode range (400/401/403/404/409/422/429/500/503/504), an ErrorResponseSchema carrying the cause field of SuaveplanError.toJSON(), an instance-only SchemaRegistry (no global singleton) with extend/partial/pick/omit guarded by OPENAPI_INVALID_BASE_SCHEMA, mergeOpenAPIDocuments / prefixPaths / filterPathsByTag / removeInternalRoutes transforms, and a definePluginRoutes helper that registers atomically against both Hono and the registry.
|
|
|
|
OpenAPI 3.1 registry, typed `RouteBuilder`, schema registry, and
|
|
document merger for the Suaveplan API. Single source of truth that
|
|
wires into Hono via `OpenAPIRegistry.applyTo(app)`, eliminating drift
|
|
between registry introspection and the served `/openapi.json`.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [emitter](./src/emitter/emitter.md) — `createOpenapiEmitter` is the aggregator the spec calls for: it ingests every plugin's manifest, every registered route…
|
|
- [emitter](./src/emitter/emitter.md) — `createOpenapiEmitter` is the aggregator the spec calls for: it ingests every plugin's manifest, every registered route…
|
|
- [endpoints](./src/endpoints/endpoints.md) — `endpoints` mounts the four HTTP routes the design doc reserves for the OpenAPI surface — `/openapi.json`, `/openapi.ya…
|
|
- [endpoints](./src/endpoints/endpoints.md) — `endpoints` mounts the four HTTP routes the design doc reserves for the OpenAPI surface — `/openapi.json`, `/openapi.ya…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry and typed error class for `@suaveplan/api-openapi`. Every public failure path in…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry and typed error class for `@suaveplan/api-openapi`. Every public failure path in…
|
|
- [merger](./src/merger/merger.md) — OpenAPI document transforms — merge, prefix, filter, redact internal routes. Multi-plugin servers compose the final pub…
|
|
- [merger](./src/merger/merger.md) — OpenAPI document transforms — merge, prefix, filter, redact internal routes. Multi-plugin servers compose the final pub…
|
|
- [namespacing](./src/namespacing/namespacing.md) — The `namespacing` module turns a plugin's structural identity (`name`, `displayName`) plus a registered route's `(metho…
|
|
- [namespacing](./src/namespacing/namespacing.md) — The `namespacing` module turns a plugin's structural identity (`name`, `displayName`) plus a registered route's `(metho…
|
|
- [plugin-integration](./src/plugin-integration/plugin-integration.md) — `definePluginRoutes` is the single helper plugins call from their `onInit` to register routes. Every call atomically up…
|
|
- [plugin-integration](./src/plugin-integration/plugin-integration.md) — `definePluginRoutes` is the single helper plugins call from their `onInit` to register routes. Every call atomically up…
|
|
- [registry](./src/registry/registry.md) — Centralised OpenAPI 3.1 registry that holds `info`, `servers`, `securitySchemes`, `tags`, and `routes`. Acts as the sin…
|
|
- [registry](./src/registry/registry.md) — Centralised OpenAPI 3.1 registry that holds `info`, `servers`, `securitySchemes`, `tags`, and `routes`. Acts as the sin…
|
|
- [route-factory](./src/route-factory/route-factory.md) — Typed route construction utilities for `@hono/zod-openapi`. The `RouteBuilder` is a fluent generic that threads `TParam…
|
|
- [route-factory](./src/route-factory/route-factory.md) — Typed route construction utilities for `@hono/zod-openapi`. The `RouteBuilder` is a fluent generic that threads `TParam…
|
|
- [schema-registry](./src/schema-registry/schema-registry.md) — Instance-based registry for reusable Zod schemas. Each schema is bound to a stable name and may be derived into related…
|
|
- [common-schemas](./src/schema-registry/common-schemas.md) — Library of common Zod schemas decorated with OpenAPI `example` metadata. Covers the universal HTTP query / path / pagin…
|
|
- [schema-registry](./src/schema-registry/schema-registry.md) — Instance-based registry for reusable Zod schemas. Each schema is bound to a stable name and may be derived into related…
|
|
- [strictness](./src/strictness/strictness.md) — `strictness` resolves the host's missing-schema policy for the OpenAPI emitter and applies it consistently across every…
|
|
- [strictness](./src/strictness/strictness.md) — `strictness` resolves the host's missing-schema policy for the OpenAPI emitter and applies it consistently across every…
|
|
- [types](./src/types/openapi.md) — Structural TypeScript types for OpenAPI 3.1 documents, supporting shapes (info / server / security scheme), registered-…
|
|
- [openapi](./src/types/openapi.md) — Structural TypeScript types for OpenAPI 3.1 documents, supporting shapes (info / server / security scheme), registered-…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Features
|
|
|
|
- **`OpenAPIRegistry`** — tracks `info`, `servers`, `securitySchemes`,
|
|
`tags`, `routes`. `buildDocument()` emits a full 3.1 document.
|
|
`applyTo(app)` wires a Hono GET handler that serves `buildDocument()`
|
|
output — no drift possible.
|
|
- **`RouteBuilder`** — fluent generic that threads
|
|
`TParams`/`TQuery`/`TBody`/`TResponse` through to the final
|
|
`TypedRouteConfig`. Hono's `app.openapi(route, handler)` then infers
|
|
`c.req.valid()` to the exact Zod type.
|
|
- **`StandardResponses`** — covers the full SuaveplanError statusCode
|
|
range (400/401/403/404/409/422/429/500/503/504).
|
|
- **`ErrorResponseSchema`** — 1:1 match for `SuaveplanError.toJSON()`
|
|
including the `cause` field.
|
|
- **`SchemaRegistry`** — instance-based (no singleton),
|
|
`extend/partial/pick/omit` derivation helpers that raise typed
|
|
`OPENAPI.SCHEMA_NOT_FOUND` / `OPENAPI.INVALID_BASE_SCHEMA` errors.
|
|
- **`CommonSchemas`** — `uuid`, `email`, `url`, `datetime`,
|
|
`positiveInt`, `nonNegativeInt`, `page`, `limit`, `sortOrder`.
|
|
- **`mergeOpenAPIDocuments` / `prefixPaths` / `filterPathsByTag` /
|
|
`removeInternalRoutes`** — pure document transforms.
|
|
- **`definePluginRoutes(routes, { app, registry, pluginName })`** —
|
|
atomic dual-surface registration; the only way a plugin should add
|
|
routes.
|
|
|
|
## Quick start
|
|
|
|
### Build a typed route
|
|
|
|
```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();
|
|
```
|
|
|
|
### With auth + body validation
|
|
|
|
```ts
|
|
import { POST, successResponse } from "@suaveplan/api-openapi/route-factory";
|
|
|
|
const CreateUser = POST("/users")
|
|
.tags("users")
|
|
.operationId("createUser")
|
|
.security([{ Bearer: [] }])
|
|
.body(z.object({ email: z.string().email() }))
|
|
.response(successResponse(z.object({ id: z.string() })))
|
|
.build();
|
|
```
|
|
|
|
### Paginated response
|
|
|
|
```ts
|
|
import { GET, paginatedResponse } from "@suaveplan/api-openapi/route-factory";
|
|
|
|
const ListUsersPaginated = GET("/users")
|
|
.tags("users")
|
|
.query(
|
|
z.object({
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
})
|
|
)
|
|
.response(paginatedResponse(z.object({ id: z.string() })))
|
|
.build();
|
|
```
|
|
|
|
### Schema registry derivation
|
|
|
|
```ts
|
|
import z from "zod";
|
|
import { createSchemaRegistry } from "@suaveplan/api-openapi/schema-registry";
|
|
|
|
const reg = createSchemaRegistry();
|
|
reg.register(
|
|
"User",
|
|
z.object({
|
|
id: z.string().uuid(),
|
|
email: z.string().email(),
|
|
password: z.string(),
|
|
})
|
|
);
|
|
|
|
const UserPublic = reg.omit("User", "UserPublic", ["password"]);
|
|
const UserPatch = reg.partial("User", "UserPatch");
|
|
const UserAudit = reg.extend("User", "UserAudit", {
|
|
lastSeenAt: z.string().datetime(),
|
|
});
|
|
```
|
|
|
|
### Merge multiple plugin docs
|
|
|
|
```ts
|
|
import {
|
|
mergeOpenAPIDocuments,
|
|
prefixPaths,
|
|
removeInternalRoutes,
|
|
} from "@suaveplan/api-openapi/merger";
|
|
|
|
const final = removeInternalRoutes(
|
|
mergeOpenAPIDocuments(
|
|
baseDoc,
|
|
prefixPaths(usersDoc, "/v1"),
|
|
prefixPaths(projectsDoc, "/v1")
|
|
)
|
|
);
|
|
```
|
|
|
|
### Plugin pattern with `definePluginRoutes`
|
|
|
|
```ts
|
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
import { createOpenAPIRegistry } from "@suaveplan/api-openapi/registry";
|
|
import { definePluginRoutes } from "@suaveplan/api-openapi/plugin-integration";
|
|
|
|
const app = new OpenAPIHono();
|
|
const registry = createOpenAPIRegistry({
|
|
info: { title: "Public API", version: "1.0.0" },
|
|
});
|
|
|
|
definePluginRoutes(
|
|
[
|
|
{
|
|
route: ListUsers,
|
|
handler: (c) => c.json({ success: true as const, data: [] }, 200),
|
|
},
|
|
{
|
|
route: CreateUser,
|
|
handler: (c) =>
|
|
c.json(
|
|
{ success: true as const, data: { id: "1" } },
|
|
200
|
|
),
|
|
},
|
|
],
|
|
{ app, registry, pluginName: "users" }
|
|
);
|
|
|
|
// Single line connects registry → Hono. No drift possible.
|
|
registry.applyTo(app);
|
|
```
|
|
|
|
## Tier
|
|
|
|
Server-tier (Hono-typed). Runs in Bun + Node.
|
|
|
|
## Modules
|
|
|
|
- **`./error-codes`** — stable `OPENAPI.*` registry (7 codes) +
|
|
`OpenAPIError` class.
|
|
- **`./types`** — structural `OpenAPIDocument`, `Operation`, `PathItem`,
|
|
`RegisteredRoute`, security-scheme types, `HTTP_METHODS`.
|
|
- **`./registry`** — `OpenAPIRegistry`, `createOpenAPIRegistry`,
|
|
`applyTo(app)` (Hono GET handler), default `BearerAuthScheme` /
|
|
`ApiKeyScheme`.
|
|
- **`./route-factory`** — fluent `RouteBuilder` (`GET`/`POST`/`PUT`/
|
|
`PATCH`/`DELETE`), `successResponse` / `errorResponse` /
|
|
`paginatedResponse`, `StandardResponses` (400-504),
|
|
`ErrorResponseSchema` matching `SuaveplanError.toJSON()`.
|
|
- **`./schema-registry`** — instance-based `SchemaRegistry` with
|
|
`extend`/`partial`/`pick`/`omit` derivation + `CommonSchemas`.
|
|
- **`./merger`** — `mergeOpenAPIDocuments`, `prefixPaths`,
|
|
`filterPathsByTag`, `removeInternalRoutes`, `extractOpenAPIDoc`.
|
|
- **`./plugin-integration`** — `definePluginRoutes` (atomic
|
|
dual-surface registration; the only way a plugin should add routes).
|
|
- **`./namespacing`** — `assignOperationIds`, `tagFor`,
|
|
`componentPrefix`, `normaliseRoutePath`. Deterministic operationId
|
|
(`<plugin>.<method>.<oasPath>`) with `OPENAPI.OPERATION_ID_COLLISION`
|
|
on duplicates.
|
|
- **`./strictness`** — `resolveStrictness`, `reportMissingSchema`,
|
|
`getMissingSchemaCounter`. `warn` vs `refuse-install` policy +
|
|
`api.openapi.missing_schema_total` OTel counter.
|
|
- **`./emitter`** — `createOpenapiEmitter({ manifests, routes,
|
|
authAdapter, hostConfig })`. The Stage-8.6 aggregator: namespacing +
|
|
strictness + security inheritance + memoised JSON/YAML/filtered
|
|
output. Emits `api.openapi.emission_total` and
|
|
`api.openapi.conversion_error_total` OTel counters.
|
|
- **`./endpoints`** — `createOpenapiRouter` / `mountOpenapiEndpoints`.
|
|
Mounts `GET /openapi.json`, `/openapi.yaml`, `/docs`, `/docs/:plugin`.
|
|
|
|
## License
|
|
|
|
MIT
|
|
|