OpenAPI
Generate an OpenAPI 3.1 spec + Swagger-UI docs from your defineRestRoute descriptors and (opt-in) your rpc procedures. Your routes ARE the API docs — nothing hand-maintained.
@voltro/plugin-openapi turns your defineRestRoute descriptors — and, opt-in, your rpc procedures (queries / mutations / actions / streams) — into a live OpenAPI 3.1 document + a Swagger-UI page. The descriptors already carry method, path, input/output Schemas, summary and deprecation; the rpc procedures carry input/output/error Schemas. The plugin reads them, so there is zero hand-written API documentation to drift.
Wiring
// app.config.ts
import { openapiPlugin } from '@voltro/plugin-openapi'
import listCustomers from './routes/v1/customers.list.route'
import createCharge from './routes/v1/charge.route'
export default {
type: 'api' as const, name: 'api',
restRoutes: [listCustomers, createCharge],
plugins: [
openapiPlugin({
routes: [listCustomers, createCharge], // the same descriptors
info: { title: 'Payments API', version: '1.0.0' },
// specPath: '/openapi.json', docsPath: '/docs' (defaults; docsPath:false disables the UI)
// token: process.env.OPENAPI_DOCS_TOKEN, // opt-in Bearer gate (see Permissions)
}),
],
}Mounts GET /openapi.json (the spec) and GET /docs (Swagger UI).
What it maps
- Each route → an OpenAPI path + operation, keyed by method.
- The input Schema's
queryfields →parameters(in: query);params→ path params;body→ a JSONrequestBody. - The output Schema → the
200JSON response. summary/description→ operation metadata;deprecated→deprecated: true;example.request/example.response→ request-body / 200-response examples;sunset→ thex-sunsetextension.- The serve pipeline's real error statuses are documented per operation —
400(input decode, when the route has aninputschema),401/403(when it hasguards),410(whensunsetis set),500(always) — all referencing one sharedcomponents.schemas.VoltroHttpErrorenvelope. - When any route declares
guards, the document publishescomponents.securitySchemes.bearerAuthand guarded operations carrysecurity: [{ bearerAuth: [] }].
Schemas are converted with effect's JSONSchema.make, so your effect/Schema definitions become JSON Schema verbatim. Pure: generateOpenApiSpec(routes, info) is exported + unit-tested if you want the spec without serving it.
rpc procedures in the spec
Pass your rpc procedure descriptors via the procedures option and each query / mutation / action / stream is projected into the same spec. The framework's rpc surface is a batched JSON-RPC protocol over one endpoint (POST /rpc) — not a REST-per-resource API — so the projection is honest about that: every procedure becomes a POST /rpc/<name> operation whose body is the rpc payload (the input Schema), tagged rpc and flagged with an x-voltro-rpc extension. The operation description states the real transport; the path is a documentation projection, not a REST route you call directly.
import { openapiPlugin } from '@voltro/plugin-openapi'
import listOrders from './orders.list.query'
import createOrder from './orders.create.mutation'
import charge from './billing.charge.action'
import runAgent from './agent.run.stream'
openapiPlugin({
routes: [listCustomers], // REST routes (optional)
procedures: [listOrders, createOrder, charge, runAgent], // rpc procedures
info: { title: 'API', version: '1.0.0' },
})Per procedure kind:
- query →
POST /rpc/<name>;input→ requestBody,output→ the200response (documented as the first snapshot; the WebSocket rpc channel then pushes reactive updates). - mutation / action →
input→ requestBody,output→200. - stream → the
elementSchema is documented as a streaming response (application/x-ndjson, one element per line) — not faked as a single JSON value. - The procedure's typed error channel (its
Schema.TaggedErrorunion) becomes a422response referencing the error schema; a procedure with no declared error documents only200+500.
Mixing rpc + REST in one spec is fine — the rpc operations are grouped under the rpc tag and flagged x-voltro-rpc, so a consumer can tell them apart from real REST routes. procedures is opt-in: omit it to keep the spec REST-only.
Access control
Both routes publish your full API contract (every path, param, and schema), and both are unauthenticated by default — mount them only where that's intended. Two levers narrow the surface:
docsPath: falsehides only the Swagger-UI viewer; the spec at/openapi.jsonstays served.token(or theOPENAPI_DOCS_TOKENenv var) requiresAuthorization: Bearer <token>on both routes — a missing or mismatched header gets a401. The same shape as@voltro/plugin-prometheus's gate. Leave it unset behind a trusted network boundary; set it for a public-internet deployment.
openapiPlugin({
routes: [listCustomers, createCharge],
token: process.env.OPENAPI_DOCS_TOKEN, // unset → both routes are public
})Permissions
The plugin declares no runtime permissions — it only mounts the two GET routes built from descriptors you already declared.