openapi
| Package | @theoven/core (built in) |
| Adds to context | ctx.openapi |
| Endpoints | GET /openapi.json, GET /docs |
| Creates files | none |
| Creates tables | none |
| Status | shipped |
Install
Section titled “Install”Built into core, so there is nothing to install.
import { createApp, openapi } from '@theoven/core'
export const app = createApp().use(openapi({ info: { title: 'My API', version: '1.0.0' } }))What it does
Section titled “What it does”Generates an OpenAPI 3.1 document from the schemas your routes already declare, and serves a
browsable reference at /docs. You write nothing extra.
A documentation page that drifts from the implementation is worse than no page at all, and deriving one from the other is the only way to stop that.
Endpoints
Section titled “Endpoints”| Method | Path | Purpose | Auth |
|---|---|---|---|
GET |
/openapi.json |
the generated document | none |
GET |
/docs |
the browsable reference | none |
Both paths are configurable, and both exclude themselves from the document they describe — documenting the docs endpoint is noise in every generated client.
Configuration
Section titled “Configuration”openapi({ info: { title: 'My API', version: '1.0.0', description: 'Does things' }, servers: [{ url: 'https://api.example' }], path: '/openapi.json', ui: '/docs', // or false to omit the UI securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } }, exclude: ['/internal/metrics'],})| Option | Default | Purpose |
|---|---|---|
info |
{ title: 'API', version: '0.0.0' } |
document metadata |
servers |
— | base URLs for generated clients |
path |
/openapi.json |
where the document is served |
ui |
/docs |
where the reference is served; false to omit |
securitySchemes |
— | merged with anything auth bricks contribute |
exclude |
— | paths to leave out |
What it generates
Section titled “What it generates”| From your route | Becomes |
|---|---|
params |
path parameters, always required |
query |
query parameters, optional per the schema |
headers |
header parameters |
body |
a request body — multipart/form-data when it contains a file |
response |
responses by status code |
summary, description, tags |
operation metadata |
Two things are added for you:
422on any route that validates input, with the RFC 9457 problem shape, so a generated client knows the error type it will actually receive.- Path parameters for routes with no
paramsschema./users/:idputs{id}in the URL template, and the spec requires a matching parameter object whether or not you declared one.
HEAD routes are skipped: they are served from GET and carry no separate contract.
What it creates
Section titled “What it creates”Files: none at runtime. oven openapi --out openapi.json writes that file when you ask it to;
nothing is written otherwise.
Tables: none.
The document comes from schemas you already wrote for validation — declaring them twice is exactly the drift this avoids:
import { z } from 'zod'
export const summary = 'Fetch a user'export const tags = ['users']export const auth = trueexport const params = z.object({ id: z.uuid() })export const response = z.object({ id: z.uuid(), email: z.email(), createdAt: z.iso.datetime(),})
export default async ({ db, params }) => findUser(db, params.id)That route arrives in the document with its path parameter, its response schema, its tag, its summary, and a security requirement — none of it written twice.
Registering the brick, with the metadata a generated client needs:
import { openapi } from '@theoven/core'
export const app = createApp().use( openapi({ info: { title: 'Acme API', version: '1.2.0', description: 'Everything Acme.' }, servers: [{ url: 'https://api.acme.com', description: 'production' }], tags: [{ name: 'users', description: 'Accounts and profiles.' }], exclude: ['/internal/*'], }),)Capabilities
Section titled “Capabilities”| Paths, params, query, body, responses from Zod | ✓ |
summary, tags, auth picked up from route exports |
✓ |
| Security schemes contributed by auth bricks | ✓ — whichever provider is registered |
| RFC 9457 problem responses documented | ✓ |
| Non-Zod Standard Schema validators | described permissively, with a startup warning |
Wildcard-mounted routes (e.g. auth-better) |
✗ — cannot be described |
| Webhooks, callbacks, links | ✗ |
Using it from code
Section titled “Using it from code”app.get('/spec-summary', (ctx) => { const document = ctx.openapi.document() return { paths: Object.keys(document.paths ?? {}).length }})Or from the CLI, which is the usual way to feed a client generator:
oven openapi > openapi.jsonoven openapi --out openapi.json --title "My API" --api-version 1.0.0Limitations
Section titled “Limitations”Only Zod schemas can be described. Zod is the one Standard Schema library that emits JSON Schema today, which is exactly why it is the bundled default. Other validators still work for validation; their operations are documented permissively — the route appears with an unconstrained body — and a warning names the vendor at startup so the gap is discoverable.
Saying “any value” is honest. Inventing a shape would not be.
A document with no paths warns. The 3.1 spec permits an empty paths object, but strict
validators reject a document with no entries anywhere. Rather than fake compliance, the
generator says so — a zero-route document usually means the brick was registered before any
routes, or exclude over-matched.
How it is verified
Section titled “How it is verified”Generated documents are handed to @readme/openapi-parser in the test suite and asserted valid.
Structural tests confirm the shape we intended; only a real parser confirms the shape the
ecosystem accepts — and it has already caught two bugs that structural tests missed.
Why 3.1
Section titled “Why 3.1”OpenAPI 3.1’s schema dialect is JSON Schema 2020-12 — exactly what z.toJSONSchema emits.
Targeting 3.0 would mean down-converting every schema and losing fidelity on the way.