Guide/guides

OpenAPI

Guren generates an OpenAPI 3.1 document from your routes. The schemas come from each route's contract, and a few annotations exist only for the document. The optional @guren/openapi package writes the document to a file, and can also serve it from your app with an interactive docs UI.

OpenAPI

Guren generates an OpenAPI 3.1 document from your routes. The schemas come from each route's contract, and a few annotations exist only for the document. The optional @guren/openapi package writes the document to a file, and can also serve it from your app with an interactive docs UI.

Generating the Document

Install the optional @guren/openapi package and generate a spec from your route definitions:

bun add @guren/openapi
bunx guren openapi:generate

This reads your routes file, extracts Zod schemas and OpenAPI metadata from route contracts, and writes an OpenAPI 3.1 JSON document to .guren/openapi.gen.json.

OpenAPI Metadata

Route contracts also accept lightweight OpenAPI annotations. These are stored on the route definition and used by the optional @guren/openapi plugin to generate an OpenAPI 3.1 document.

router.post('/posts', {
  body: CreatePostSchema,
  output: PostResponseSchema,
  name: 'posts.store',
  summary: 'Create a post',
  description: 'Creates a new blog post.',
  tags: ['Posts'],
}, [PostsController, 'store'])

router.get('/posts/:id', {
  params: PostIdParams,
  name: 'posts.show',
  summary: 'Get a post',
  tags: ['Posts'],
  deprecated: false,
}, [PostsController, 'show'])

Available OpenAPI fields:

Field Type Purpose
summary string Short description shown in docs UI
description string Detailed explanation of the endpoint
tags string[] Group endpoints in the docs UI
operationId string Override the auto-generated operation ID
deprecated boolean Mark endpoint as deprecated

CLI Options

# Custom title and version
bunx guren openapi:generate --title "Blog API" --version "1.0.0"

# Custom output path
bunx guren openapi:generate --out docs/openapi.json

# Include a server URL
bunx guren openapi:generate --server "https://api.example.com"

# Overwrite existing file
bunx guren openapi:generate --force

Mounting Docs at Runtime

You can also serve the OpenAPI spec and an interactive docs UI directly from your application:

import { createApp } from '@guren/core'
import { mountOpenApiDocs } from '@guren/openapi'

const app = createApp({ routes: registerWebRoutes })

mountOpenApiDocs(app, {
  title: 'Blog API',
  version: '1.0.0',
})

This mounts two endpoints:

Path Description
/openapi.json The generated OpenAPI 3.1 JSON document
/docs Interactive API documentation UI (Scalar)

Customize the paths with jsonPath and docsPath options:

mountOpenApiDocs(app, {
  title: 'Blog API',
  version: '1.0.0',
  jsonPath: '/api/openapi.json',
  docsPath: '/api/docs',
})

When mounted on an Application instance, route definitions are read from the router automatically. For a plain Hono instance, pass definitions explicitly.

The servers option accepts a function as well as a list. A mounted document is generated per request and the function is called each time, so it can advertise an address the process does not know when it mounts the docs. With PORT=0 the operating system assigns the port, which therefore exists only once listen() has returned it — a fixed list would leave the document, and every client generated from it, pointing at an address nothing is listening on:

mountOpenApiDocs(app, {
  title: 'Blog API',
  version: '1.0.0',
  servers: () => [app.address?.url ?? 'http://localhost:3000'],
})

await app.listen({ port: 0 })

app.address is where listen() bound this app, and undefined until it has. Because the function runs per request, it reads the address after listen() has set it. A plain Hono instance has no Application to ask, so return the address however that app knows it.