Skip to content

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

Built into core, so there is nothing to install.

src/app.ts
import { createApp, openapi } from '@theoven/core'
export const app = createApp().use(openapi({ info: { title: 'My API', version: '1.0.0' } }))

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.

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.

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
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:

  • 422 on 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 params schema. /users/:id puts {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.

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:

src/routes/users/[id].get.ts
import { z } from 'zod'
export const summary = 'Fetch a user'
export const tags = ['users']
export const auth = true
export 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:

src/app.ts
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/*'],
}),
)
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 ✗
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:

Terminal window
oven openapi > openapi.json
oven openapi --out openapi.json --title "My API" --api-version 1.0.0

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.

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.

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.