API Reference for Hono
This middleware provides an easy way to render a beautiful API reference based on an OpenAPI or AsyncAPI document with Hono.

Setup Prompt
Copy this prompt into your coding agent to add Scalar to an existing Hono app with OpenAPI, AsyncAPI, or both.
Add Scalar API Reference to this Hono app using @scalar/hono-api-reference.
Read https://scalar.com/products/api-references/integrations/hono for the integration guide and https://scalar.com/products/api-references/asyncapi for AsyncAPI support before making changes.
1. Inspect the app entrypoint, runtime, package manager, HTTP routes, event-driven channels, and existing API descriptions. Use OpenAPI for HTTP request/response APIs and AsyncAPI for event-driven APIs, or both when appropriate.
2. Install @scalar/hono-api-reference using the project’s package manager.
3. Reuse existing API descriptions and generation tools. For HTTP routes without an OpenAPI description, add @hono/zod-openapi or hono-openapi to fit the app’s existing validation and routing. For event-driven APIs without an AsyncAPI description, create an AsyncAPI 3.x document from the actual servers, channels, operations, messages, and payload schemas. These OpenAPI generation libraries do not generate AsyncAPI. Do not invent endpoints, responses, channels, or messages; ask about behavior that cannot be determined from the app.
4. Serve each description as JSON or YAML at an appropriate URL, such as /openapi.json or /asyncapi.json, or reuse its existing URL. Import { Scalar } from '@scalar/hono-api-reference' and register app.get('/scalar', Scalar({ url: '/asyncapi.json' })) for an AsyncAPI document, or use the OpenAPI document URL instead. If both are present, use Scalar({ sources: [{ title: 'HTTP API', url: '/openapi.json' }, { title: 'Events', url: '/asyncapi.json' }] }). Adjust URLs for the actual document routes and base path. Register the documentation routes before catch-all handlers.
5. Preserve the app’s runtime, existing routes, event handlers, middleware, and authentication requirements. Do not replace the server entrypoint or expose protected documentation publicly.
6. Run the relevant project checks and validate each description with tooling for its format and declared version. Start the app with its existing development command. Verify that each document URL returns the expected description and /scalar renders its endpoints or channels and messages. If both formats are configured, verify that both are available in the document selector.
7. Summarize the changed files, checks performed, and local documentation URL.
Installation
npm install @scalar/hono-api-reference
Usage
Set up Zod OpenAPI Hono or Hono OpenAPI and pass the configured URL to the Scalar middleware:
import { Hono } from 'hono'
import { Scalar } from '@scalar/hono-api-reference'
// Type your runtime bindings once (for example Cloudflare Workers env vars)
type Bindings = {
ENVIRONMENT: string
}
const app = new Hono<{ Bindings: Bindings }>()
// Use the middleware to serve the API Reference at /scalar
app.get('/scalar', Scalar({ url: '/doc' }))
// Or resolve the configuration per request from the context. Pass the same
// bindings to `Scalar` so `c.env` is fully typed:
app.get(
'/scalar',
Scalar<{ Bindings: Bindings }>((c) => ({
url: '/doc',
proxyUrl: c.env.ENVIRONMENT === 'development' ? 'https://proxy.scalar.com' : undefined,
})),
)
export default app
The Hono middleware takes our universal configuration object, read more about configuration in the core package README.
Serve the reference and document together
With the middleware above you serve the API document on one route and point the reference at it on another. If you generate your document with Zod OpenAPI Hono, Scalar.serve does both from a single mount, so you do not have to add a separate document route and keep its url in sync:
import { OpenAPIHono } from '@hono/zod-openapi'
import { Scalar } from '@scalar/hono-api-reference'
const app = new OpenAPIHono()
// ... register your routes ...
// Renders the reference at /scalar and serves the document at /scalar/openapi.json
app.route(
'/scalar',
Scalar.serve({
document: () =>
app.getOpenAPI31Document({
openapi: '3.1.0',
info: { title: 'Example', version: 'v1' },
}),
}),
)
export default app
document can be an OpenAPI or AsyncAPI object, or a synchronous or asynchronous function that returns it. The function receives the Hono Context, so you can read c.env or c.req and build the document per request. Pass any other configuration option (such as theme or pageTitle) alongside document, and change where the JSON is served with documentPath (defaults to /openapi.json).
AsyncAPI and WebSocket apps
The same integration renders OpenAPI and AsyncAPI. If your app already serves an AsyncAPI document, point Scalar at its URL:
import { Scalar } from '@scalar/hono-api-reference'
import { Hono } from 'hono'
const app = new Hono()
// Your app serves its existing AsyncAPI document at /asyncapi.json.
app.get('/reference', Scalar({ url: '/asyncapi.json' }))
To serve both the reference and an existing document, use Scalar.serve(). This minimal AsyncAPI 3.0 example describes a WebSocket chat server receiving text messages:
import { Scalar } from '@scalar/hono-api-reference'
import { Hono } from 'hono'
const app = new Hono()
const document = {
asyncapi: '3.0.0',
info: { title: 'WebSocket Chat', version: '1.0.0' },
servers: {
local: { host: 'localhost:3000', protocol: 'ws' },
},
channels: {
chat: {
address: '/chat',
servers: [{ $ref: '#/servers/local' }],
messages: {
text: { payload: { type: 'string' } },
},
},
},
operations: {
receiveMessage: {
action: 'receive',
channel: { $ref: '#/channels/chat' },
messages: [{ $ref: '#/channels/chat/messages/text' }],
},
},
}
app.route(
'/reference',
Scalar.serve({ document, documentPath: '/asyncapi.json' }),
)
export default app
This serves the reference at /reference and JSON at /reference/asyncapi.json. You can also pass a document factory, such as document: () => document, including an asynchronous factory. Factories receive the Hono Context and run when the JSON document is requested. If you mount the app under /api, the reference automatically points to /api/reference/asyncapi.json.
documentPath defaults to /openapi.json for both formats; set it explicitly as above for an AsyncAPI URL. Scalar serves and renders the document you supply. It does not generate AsyncAPI from Hono routes or WebSocket handlers, or implement the described WebSocket server.
See the AsyncAPI documentation for supported versions, rendering features, and current limitations.
Themes
The middleware ships with a custom Hono theme that is applied by default, so there is nothing to opt into and no hono theme token to set. To use a different look, set theme to one of the predefined themes — alternate, default, moon, purple, solarized, bluePlanet, deepSpace, saturn, kepler, mars, or laserwave — or set it to none to drop the default Hono theme and start from a blank slate. All themes come with a light and dark color scheme.
import { Scalar } from '@scalar/hono-api-reference'
// Switch the theme (or pass other options)
app.get('/scalar', Scalar({
url: '/doc',
theme: 'purple',
}))
Using hono-openapi
The example above uses Zod OpenAPI Hono. If you prefer hono-openapi, it generates the document as middleware, so you can add it to an existing app without rewriting your routes. Describe routes with describeRoute, serve the generated document with openAPIRouteHandler, and point Scalar at it:
import { Hono } from 'hono'
import { describeRoute, openAPIRouteHandler } from 'hono-openapi'
import { Scalar } from '@scalar/hono-api-reference'
const app = new Hono()
app.get(
'/',
describeRoute({
summary: 'Say hello',
responses: { 200: { description: 'OK' } },
}),
(c) => c.text('Hello!'),
)
// Serve the generated OpenAPI document
app.get(
'/openapi.json',
openAPIRouteHandler(app, {
documentation: {
info: { title: 'Example', version: '1.0.0' },
},
}),
)
// Render the reference from it
app.get('/scalar', Scalar({ url: '/openapi.json' }))
export default app
Runtimes
Hono runs on many runtimes, and the Scalar middleware works on all of them — it only renders HTML, so nothing here is Node-specific. Only the server entrypoint changes.
Cloudflare Workers, Bun, and Deno export the app directly:
app.get('/scalar', Scalar({ url: '/doc' }))
export default app
Deno can also serve it explicitly:
Deno.serve(app.fetch)
Node.js uses @hono/node-server:
import { serve } from '@hono/node-server'
serve({ fetch: app.fetch, port: 3000 })
Custom page title
There's one additional option to set the page title:
import { Scalar } from '@scalar/hono-api-reference'
// Set a page title
app.get('/scalar', Scalar({
url: '/doc',
pageTitle: 'Awesome API',
}))
Custom CDN
You can use a custom CDN, default is https://cdn.jsdelivr.net/npm/@scalar/api-reference.
You can also pin the CDN to a specific version by specifying it in the CDN string like https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.25.28
You can find all available CDN versions here
import { Scalar } from '@scalar/hono-api-reference'
app.get('/scalar', Scalar({ url: '/doc', pageTitle: 'Awesome API' }))
app.get('/scalar', Scalar({
url: '/doc',
cdn: 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest',
}))
Markdown for LLMs
If you want to create a Markdown version of the API reference (for LLMs), install @scalar/openapi-to-markdown:
npm install @scalar/openapi-to-markdown
And add an additional route for it:
import { Hono } from 'hono'
import { createMarkdownFromOpenApi } from '@scalar/openapi-to-markdown'
const app = new Hono()
// Generate Markdown from your OpenAPI document
const markdown = await createMarkdownFromOpenApi(content)
/**
* Register a route to serve the Markdown for LLMs
*
* Q: Why /llms.txt?
* A: It's a proposal to standardise on using an /llms.txt file.
*
* @see https://llmstxt.org/
*/
app.get('/llms.txt', (c) => c.text(markdown))
export default app
Or, if you are using Zod OpenAPI Hono:
// Get the OpenAPI document
const content = app.getOpenAPI31Document({
openapi: '3.1.0',
info: { title: 'Example', version: 'v1' },
})
const markdown = await createMarkdownFromOpenApi(JSON.stringify(content))
app.get('/llms.txt', async (c) => {
return c.text(markdown)
})
Publish to the Scalar Registry
If you generate your OpenAPI document from Zod OpenAPI Hono, you can publish it to the Scalar Registry to power hosted docs, share it with your team, and keep versions in sync. You do not need a running server for this — OpenAPIHono can build the document straight from your Zod-driven routes.
Add a small script that calls getOpenAPI31Document() and writes the result to a file:
// scripts/generate-openapi.ts
import { writeFileSync } from 'node:fs'
import { app } from '../src/app'
const document = app.getOpenAPI31Document({
openapi: '3.1.0',
info: {
title: 'Example API',
version: '0.1.0',
},
servers: [{ url: process.env.BASE_URL ?? 'https://example.com' }],
})
writeFileSync('openapi.json', `${JSON.stringify(document, null, 2)}\n`)
Here app is your OpenAPIHono instance with all routes registered. This is the same document you would serve at runtime with app.doc31('/openapi.json', ...), just written to a file instead.
Wire it up with a couple of scripts. The Scalar CLI handles validation and publishing:
{
"scripts": {
"openapi:generate": "tsx scripts/generate-openapi.ts openapi.json",
"scalar:publish": "pnpm openapi:generate && scalar registry publish openapi.json --namespace your-namespace --slug your-api"
}
}
To keep the registry up to date automatically, run the same steps in CI whenever a route or schema changes:
# .github/workflows/scalar.yml
name: Publish API docs
on:
push:
branches: [main]
# Only republish when something that can change the document changes
paths:
- 'src/**'
- 'scripts/generate-openapi.ts'
workflow_dispatch: {}
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
# @scalar/cli requires Node >= 24
node-version: 24
cache: pnpm
- name: Install
run: pnpm install --frozen-lockfile
- name: Generate OpenAPI document
run: pnpm openapi:generate
- name: Validate document
run: pnpm dlx @scalar/cli document validate openapi.json
- name: Publish to Scalar Registry
env:
SCALAR_API_KEY: ${{ secrets.SCALAR_API_KEY }}
run: |
pnpm dlx @scalar/cli auth login --token "$SCALAR_API_KEY"
# Publish under the document's own info.version so a docs route can
# pin a stable version. --force overwrites that version on every push.
VERSION="$(node -p "require('./openapi.json').info.version")"
pnpm dlx @scalar/cli registry publish openapi.json \
--namespace your-namespace --slug your-api \
--version "$VERSION" --force
What did you think of this content?