4.6 KiB
plugin-integration
(no submodules)
Purpose
definePluginRoutes is the single helper plugins call from their onInit
to register routes. Every call atomically updates both the Hono
OpenAPIHono app (via app.openapi(route, handler)) and the
OpenAPIRegistry (via registry.registerRoute(...)). The two surfaces
cannot drift — there is no path through the helper that touches only
one of them.
Eliminates a class of upstream bugs where a plugin would either register against Hono but forget the registry (operation served but not discoverable) or the inverse (discoverable but not served).
Features
- Atomically registers each route against both Hono and the registry.
- Records
pluginNameon every registry entry for laterregistry.getRoutesByPlugin(name)lookups. - Carries
operationId,summary,description,deprecated, andsecuritythrough into the registry record. - Returns the list of
RegisteredRouteentries for caller-side post-processing. - Defensive-copies
tagsandsecurityarrays so the route config remains immutable from the registry's perspective.
Usage: Basic
import { OpenAPIHono, z } from "@hono/zod-openapi";
import { createOpenAPIRegistry } from "@suaveplan/api-openapi/registry";
import { GET, successResponse } from "@suaveplan/api-openapi/route-factory";
import { definePluginRoutes } from "@suaveplan/api-openapi/plugin-integration";
const app = new OpenAPIHono();
const registry = createOpenAPIRegistry({
info: { title: "X", version: "1.0.0" },
});
const ListUsers = GET("/users")
.tags("users")
.operationId("listUsers")
.response(successResponse(z.array(z.object({ id: z.string() }))))
.build();
definePluginRoutes(
[
{
route: ListUsers,
handler: (c) => c.json({ success: true as const, data: [] }, 200),
},
],
{ app, registry, pluginName: "users" }
);
Usage: Advanced
import { definePluginRoutes } from "@suaveplan/api-openapi/plugin-integration";
import { definePlugin } from "@suaveplan/api-plugin";
interface MyServices {
app: OpenAPIHono;
registry: OpenAPIRegistry;
}
export const projectsPlugin = definePlugin<{
/* config shape */
}>({
name: "projects",
version: "0.1.0",
onInit(ctx) {
const app = ctx.getService("app") as MyServices["app"];
const registry = ctx.getService("registry") as MyServices["registry"];
const created = definePluginRoutes(
[
/* { route, handler } entries built with RouteBuilder */
],
{ app, registry, pluginName: "projects" }
);
ctx.logger.info(`projects: registered ${created.length} routes`);
},
});
API Reference
type RouteHandler = (c: Context) => Response | Promise<Response> | unknowninterface PluginRouteEntry { route: RouteConfig; handler: RouteHandler }interface DefinePluginRoutesOptions { app: OpenAPIHono; registry: OpenAPIRegistry; pluginName: string }function definePluginRoutes(entries, options): RegisteredRoute[]
Implementation Notes
The helper iterates the entries sequentially — there is no parallelism.
Hono's app.openapi is synchronous and side-effecting, so concurrency
would gain nothing. Sequential iteration also gives well-defined
ordering in registry.getRoutesByPlugin(name): the array reflects the
order routes were declared in the plugin source, which is the order
authors expect when consuming the registry for documentation or
introspection.
The RegisteredRoute record copies the route's tags and security
arrays defensively. A plugin could otherwise mutate the array on its
RouteConfig post-registration and silently alter the registry's
introspection view.
route.method is passed through to the registry as-is. Hono normalises
methods to lowercase, so the registry receives lowercase, matching the
expectations of OpenAPIRegistry.buildDocument().
The helper is intentionally a free function (not a method on the registry). Decoupling it from the registry class means plugins can substitute their own "register-both-surfaces" implementation in tests without touching the registry contract.
The handler type is (c: Context) => Response | Promise<Response> | unknown
— Hono's actual handler signature is broader (typed under generics
that depend on the specific RouteConfig). Trying to surface that
inference at the helper boundary would require every caller to thread
the same generics, defeating the helper's purpose. We accept the loose
signature and trust the call site of app.openapi to type-check the
handler against the route's contract.