@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.
Documentation tree
- emitter —
createOpenapiEmitteris the aggregator the spec calls for: it ingests every plugin's manifest, every registered route…- emitter —
createOpenapiEmitteris the aggregator the spec calls for: it ingests every plugin's manifest, every registered route…
- emitter —
- endpoints —
endpointsmounts the four HTTP routes the design doc reserves for the OpenAPI surface —/openapi.json, `/openapi.ya…- endpoints —
endpointsmounts the four HTTP routes the design doc reserves for the OpenAPI surface —/openapi.json, `/openapi.ya…
- endpoints —
- error-codes — Defines the stable error-code registry and typed error class for
@suaveplan/api-openapi. Every public failure path in…- error-codes — Defines the stable error-code registry and typed error class for
@suaveplan/api-openapi. Every public failure path in…
- error-codes — Defines the stable error-code registry and typed error class for
- merger — OpenAPI document transforms — merge, prefix, filter, redact internal routes. Multi-plugin servers compose the final pub…
- merger — OpenAPI document transforms — merge, prefix, filter, redact internal routes. Multi-plugin servers compose the final pub…
- namespacing — The
namespacingmodule turns a plugin's structural identity (name,displayName) plus a registered route's `(metho…- namespacing — The
namespacingmodule turns a plugin's structural identity (name,displayName) plus a registered route's `(metho…
- namespacing — The
- plugin-integration —
definePluginRoutesis the single helper plugins call from theironInitto register routes. Every call atomically up…- plugin-integration —
definePluginRoutesis the single helper plugins call from theironInitto register routes. Every call atomically up…
- plugin-integration —
- registry — Centralised OpenAPI 3.1 registry that holds
info,servers,securitySchemes,tags, androutes. Acts as the sin…- registry — Centralised OpenAPI 3.1 registry that holds
info,servers,securitySchemes,tags, androutes. Acts as the sin…
- registry — Centralised OpenAPI 3.1 registry that holds
- route-factory — Typed route construction utilities for
@hono/zod-openapi. TheRouteBuilderis a fluent generic that threads `TParam…- route-factory — Typed route construction utilities for
@hono/zod-openapi. TheRouteBuilderis a fluent generic that threads `TParam…
- route-factory — Typed route construction utilities for
- schema-registry — Instance-based registry for reusable Zod schemas. Each schema is bound to a stable name and may be derived into related…
- common-schemas — Library of common Zod schemas decorated with OpenAPI
examplemetadata. Covers the universal HTTP query / path / pagin… - schema-registry — Instance-based registry for reusable Zod schemas. Each schema is bound to a stable name and may be derived into related…
- common-schemas — Library of common Zod schemas decorated with OpenAPI
- strictness —
strictnessresolves the host's missing-schema policy for the OpenAPI emitter and applies it consistently across every…- strictness —
strictnessresolves the host's missing-schema policy for the OpenAPI emitter and applies it consistently across every…
- strictness —
- types — Structural TypeScript types for OpenAPI 3.1 documents, supporting shapes (info / server / security scheme), registered-…
- openapi — Structural TypeScript types for OpenAPI 3.1 documents, supporting shapes (info / server / security scheme), registered-…
Features
OpenAPIRegistry— tracksinfo,servers,securitySchemes,tags,routes.buildDocument()emits a full 3.1 document.applyTo(app)wires a Hono GET handler that servesbuildDocument()output — no drift possible.RouteBuilder— fluent generic that threadsTParams/TQuery/TBody/TResponsethrough to the finalTypedRouteConfig. Hono'sapp.openapi(route, handler)then infersc.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 forSuaveplanError.toJSON()including thecausefield.SchemaRegistry— instance-based (no singleton),extend/partial/pick/omitderivation helpers that raise typedOPENAPI.SCHEMA_NOT_FOUND/OPENAPI.INVALID_BASE_SCHEMAerrors.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
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
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
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
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
import {
mergeOpenAPIDocuments,
prefixPaths,
removeInternalRoutes,
} from "@suaveplan/api-openapi/merger";
const final = removeInternalRoutes(
mergeOpenAPIDocuments(
baseDoc,
prefixPaths(usersDoc, "/v1"),
prefixPaths(projectsDoc, "/v1")
)
);
Plugin pattern with definePluginRoutes
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— stableOPENAPI.*registry (7 codes) +OpenAPIErrorclass../types— structuralOpenAPIDocument,Operation,PathItem,RegisteredRoute, security-scheme types,HTTP_METHODS../registry—OpenAPIRegistry,createOpenAPIRegistry,applyTo(app)(Hono GET handler), defaultBearerAuthScheme/ApiKeyScheme../route-factory— fluentRouteBuilder(GET/POST/PUT/PATCH/DELETE),successResponse/errorResponse/paginatedResponse,StandardResponses(400-504),ErrorResponseSchemamatchingSuaveplanError.toJSON()../schema-registry— instance-basedSchemaRegistrywithextend/partial/pick/omitderivation +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>) withOPENAPI.OPERATION_ID_COLLISIONon duplicates../strictness—resolveStrictness,reportMissingSchema,getMissingSchemaCounter.warnvsrefuse-installpolicy +api.openapi.missing_schema_totalOTel counter../emitter—createOpenapiEmitter({ manifests, routes, authAdapter, hostConfig }). The Stage-8.6 aggregator: namespacing + strictness + security inheritance + memoised JSON/YAML/filtered output. Emitsapi.openapi.emission_totalandapi.openapi.conversion_error_totalOTel counters../endpoints—createOpenapiRouter/mountOpenapiEndpoints. MountsGET /openapi.json,/openapi.yaml,/docs,/docs/:plugin.
License
MIT