For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/services/openapi.md.

OpenAPI and API docs

@dotpress/openapi builds an OpenAPI 3.1 document from the routes of your app, and serves an interactive API reference with Scalar. No annotation: the paths, parameters, bodies and responses come from the Zod schemas of the routes, and the security from their middlewares. The documentation cannot drift from the code.

npm
yarn
pnpm
bun
deno
npm install @dotpress/openapi
import { openapiPlugin } from '@dotpress/openapi'

const app = await createApp({
  adapter,
  plugins: [
    authPlugin(auth),
    openapiPlugin({ info: { title: 'Tasks API', version: '1.0.0' } }),
  ],
})
// GET /openapi.json: the document
// GET /docs: the API reference

The document is generated once, when the app is created.

What is documented

SourceOpenAPI
path (/users/:id)the path (/users/{id}), with the base path of the app
schema.params, schema.querypath and query parameters. Path parameters without schema are strings.
schema.bodythe JSON request body, required unless the schema is optional
schema.filesa multipart/form-data body, with the body fields
schema.response, statusthe success response (200 by default)
meta.responsesother responses
middlewares, meta.securitythe security requirements
meta.summary, description, tags…the operation. meta.hidden excludes the route.
—the standard errors: 400 for routes with request schemas, 401 and 403 for secured routes, 500, with the DotpressError schema

Request schemas are documented as the client sends them (z.coerce.number() is a number, a field with a default is optional), response schemas as the API returns them. Dates are ISO 8601 strings.

Describe the routes

defineRoute({
  method: 'post',
  path: '/tasks',
  status: 201,
  schema: (z) => ({ body: TaskInput, response: Task }),
  meta: {
    summary: 'Create a task',
    description: 'The task is added to the project of the current user.',
    tags: ['tasks'],
    responses: { 409: 'A task with this title already exists' },
  },
  handler,
})

meta.responses documents the responses the schemas do not describe: errors returned by the handler, or bodies that are not JSON. An entry is a description, or an object with description, schema and contentType:

meta: {
  responses: {
    200: { description: 'CSV export', contentType: 'text/csv' },
    404: 'Member not found',
    409: { description: 'Already exported', schema: ExportStatus },
  },
}

An entry replaces the generated response of the same status.

Reusable schemas

A Zod schema with an identifier is declared once in components/schemas, and referenced by the routes:

const Task = z
  .object({ id: z.uuid(), title: z.string(), done: z.boolean() })
  .meta({ id: 'Task', description: 'A task to do' })

When the input and output of a schema differ (defaults, transforms, the additionalProperties: false of the output of objects), the request side gets an Input variant: Task and TaskInput. Two different schemas with the same identifier fail the generation. Recursive schemas need an identifier.

Security

A route is secured by the security schemes its middlewares declare, including group and global middlewares. requireAuth, requireRole() and requireClient() of @dotpress/auth declare bearerAuth, and the auth plugin declares the scheme: the API reference shows an Authorize button, and the secured routes a lock.

Your own middlewares declare theirs:

const requireApiKey = defineMiddleware(checkApiKey, { security: ['apiKey'] })

openapiPlugin({
  info,
  securitySchemes: { apiKey: { type: 'apiKey', in: 'header', name: 'x-api-key' } },
})

meta.security overrides the schemes of a route, and [] marks it as public. A scheme used by a route but not declared is reported as a warning, like a schema without JSON representation: the plugin logs them with its logger.

Options

OptionDefaultDescription
info—Required. title, version, and the other fields of the OpenAPI info object.
serversthe public base path of the appThe servers of the document.
tags—Descriptions of the tags used by the routes.
externalDocs—A link to external documentation.
securitySchemes—Security schemes, added to those of the plugins.
path/openapi.jsonThe path of the document.
ui{ path: '/docs' }The Scalar page: path, title, configuration, cdnUrl, integrity. false serves the document only.
middlewares[]Middlewares of the document and page routes, for instance to restrict them.
Public or private documentation?

The document lists every route of the API, with its parameters. For an internal API, protect it with middlewares: [requireRole('admin')], or serve it in development only.

The API reference

The page loads a pinned version of Scalar from jsDelivr, checked with Subresource Integrity. It is neutral by default (SCALAR_NEUTRAL_CONFIGURATION): no telemetry, no AI agent or MCP integration, no developer toolbar, no link to the Scalar client, system fonts, and no "Powered by Scalar" link. ui.configuration overrides these settings with the Scalar configuration, and its customCss is appended. cdnUrl hosts the script yourself. With a strict Content Security Policy, the page needs an inline script.

The document in CI

dotpress-openapi writes the document without starting the server, to commit it, check its changes in pull requests, or generate clients:

npx dotpress-openapi src/app.ts --export createApp --out openapi.json

The module exports the app, or a function creating it; --export chooses the export (default by default). The routes are mounted, no server is started, and the app is closed afterwards (onClose hooks).

  • With openapiPlugin(), its document is used as is. Otherwise, the document is generated with --title and --api-version, or the name and version of the package.json of the current directory.
  • Without --out, the document is printed on the standard output. Warnings and errors go to the standard error, with a non-zero exit code on failure.
  • TypeScript modules are loaded with the type stripping of Node.js 22.18 or later. Otherwise, point to the compiled module, or run the command with NODE_OPTIONS="--import tsx".

generateOpenApi(app, options) returns the document in code, with the options of the plugin except path, ui and middlewares, plus onWarning (console.warn by default):

import { generateOpenApi } from '@dotpress/openapi'

const document = generateOpenApi(app, { info: { title: 'Tasks API', version: '1.0.0' } })

Example

Every example serves its document and reference. The Express example shows reusable schemas and documented errors; the Auth example, secured routes.