Hono OpenAPI Starter automatically generates OpenAPI 3.0 documentation from your route definitions. This guide explains how the integration works and how to leverage it for interactive API documentation.
Overview
The OpenAPI integration works through a simple flow:
Every route defined with createRoute() is automatically included in the OpenAPI spec. There’s no separate documentation step—your code is the documentation.
Configuration
OpenAPI is configured once in your application:
src/lib/configure-open-api.ts
import { Scalar } from "@scalar/hono-api-reference" ;
import type { AppOpenAPI } from "./types" ;
import packageJSON from "../../package.json" with { type : "json" };
export default function configureOpenAPI ( app : AppOpenAPI ) {
// Generate OpenAPI spec
app . doc ( "/doc" , {
openapi: "3.0.0" ,
info: {
version: packageJSON . version ,
title: "Tasks API" ,
},
});
// Serve interactive documentation
app . get ( "/reference" , Scalar ({
url: "/doc" ,
theme: "kepler" ,
layout: "classic" ,
defaultHttpClient: {
targetKey: "js" ,
clientKey: "fetch" ,
},
}));
}
This configuration:
Exposes the raw OpenAPI JSON spec at /doc
Serves beautiful interactive docs at /reference using Scalar
Pulls version info from your package.json
Sets the API title and description
The version in your OpenAPI spec is automatically synced with package.json, ensuring your API version is always accurate.
Route Documentation
Basic Route Definition
Every route created with createRoute() contributes to the OpenAPI spec:
src/routes/tasks/tasks.routes.ts
import { createRoute , z } from "@hono/zod-openapi" ;
import * as HttpStatusCodes from "stoker/http-status-codes" ;
import { jsonContent } from "stoker/openapi/helpers" ;
export const list = createRoute ({
path: "/tasks" ,
method: "get" ,
tags: [ "Tasks" ],
responses: {
[HttpStatusCodes. OK ]: jsonContent (
z . array ( selectTasksSchema ),
"The list of tasks" ,
),
},
});
This generates an OpenAPI operation with:
Path: /tasks
Method: GET
Tag: Tasks (groups related endpoints)
Response schema for 200 OK
What does the generated OpenAPI spec look like?
{
"openapi" : "3.0.0" ,
"info" : {
"title" : "Tasks API" ,
"version" : "1.0.0"
},
"paths" : {
"/tasks" : {
"get" : {
"tags" : [ "Tasks" ],
"responses" : {
"200" : {
"description" : "The list of tasks" ,
"content" : {
"application/json" : {
"schema" : {
"type" : "array" ,
"items" : {
"type" : "object" ,
"properties" : {
"id" : { "type" : "number" },
"name" : { "type" : "string" },
"done" : { "type" : "boolean" },
"createdAt" : { "type" : "string" , "format" : "date-time" },
"updatedAt" : { "type" : "string" , "format" : "date-time" }
},
"required" : [ "id" , "name" , "done" ]
}
}
}
}
}
}
}
}
}
}
Request Body Documentation
For routes that accept data, document the request body:
src/routes/tasks/tasks.routes.ts
import { jsonContentRequired } from "stoker/openapi/helpers" ;
import { insertTasksSchema } from "@/db/schema" ;
export const create = createRoute ({
path: "/tasks" ,
method: "post" ,
request: {
body: jsonContentRequired (
insertTasksSchema ,
"The task to create" ,
),
},
tags: [ "Tasks" ],
responses: {
[HttpStatusCodes. OK ]: jsonContent (
selectTasksSchema ,
"The created task" ,
),
},
});
Use jsonContentRequired() for required bodies and jsonContent() for optional bodies. This affects both validation and documentation.
Path Parameters
Document path parameters using Zod schemas:
src/routes/tasks/tasks.routes.ts
import { IdParamsSchema } from "stoker/openapi/schemas" ;
export const getOne = createRoute ({
path: "/tasks/{id}" ,
method: "get" ,
request: {
params: IdParamsSchema , // { id: number }
},
tags: [ "Tasks" ],
responses: {
[HttpStatusCodes. OK ]: jsonContent (
selectTasksSchema ,
"The requested task" ,
),
[HttpStatusCodes. NOT_FOUND ]: jsonContent (
notFoundSchema ,
"Task not found" ,
),
},
});
The IdParamsSchema from Stoker validates that id is a numeric string and converts it to a number:
// From stoker/openapi/schemas
export const IdParamsSchema = z . object ({
id: z . string (). transform ( Number ). pipe ( z . number (). int (). positive ()),
});
Query Parameters
Define query parameters with Zod:
const listQuerySchema = z . object ({
limit: z . string ()
. transform ( Number )
. pipe ( z . number (). min ( 1 ). max ( 100 ))
. default ( "10" ),
offset: z . string ()
. transform ( Number )
. pipe ( z . number (). min ( 0 ))
. default ( "0" ),
status: z . enum ([ "active" , "completed" , "all" ]). default ( "all" ),
});
export const list = createRoute ({
path: "/tasks" ,
method: "get" ,
request: {
query: listQuerySchema ,
},
tags: [ "Tasks" ],
responses: {
[HttpStatusCodes. OK ]: jsonContent (
z . array ( selectTasksSchema ),
"The list of tasks" ,
),
},
});
Query parameters are always strings in HTTP. Use .transform(Number) to convert them to numbers for your handler while keeping the OpenAPI spec accurate.
Response Documentation
Multiple Response Codes
Document all possible responses:
src/routes/tasks/tasks.routes.ts
import { createErrorSchema } from "stoker/openapi/schemas" ;
import { notFoundSchema } from "@/lib/constants" ;
export const patch = createRoute ({
path: "/tasks/{id}" ,
method: "patch" ,
request: {
params: IdParamsSchema ,
body: jsonContentRequired ( patchTasksSchema , "The task updates" ),
},
tags: [ "Tasks" ],
responses: {
[HttpStatusCodes. OK ]: jsonContent (
selectTasksSchema ,
"The updated task" ,
),
[HttpStatusCodes. NOT_FOUND ]: jsonContent (
notFoundSchema ,
"Task not found" ,
),
[HttpStatusCodes. UNPROCESSABLE_ENTITY ]: jsonContent (
createErrorSchema ( patchTasksSchema )
. or ( createErrorSchema ( IdParamsSchema )),
"The validation error(s)" ,
),
},
});
This documents:
200 OK : Success response with the updated task
404 Not Found : Task doesn’t exist
422 Unprocessable Entity : Validation errors
Always document error responses. This helps API consumers handle errors properly.
Error Schemas
Stoker provides createErrorSchema() to generate consistent error response schemas:
import { createErrorSchema } from "stoker/openapi/schemas" ;
const errorSchema = createErrorSchema ( insertTasksSchema );
This creates a schema for Zod validation errors:
{
success : false ,
error : {
issues : [
{
code: string ,
path: ( string | number )[],
message: string ,
}
],
name : "ZodError" ,
}
}
No Content Responses
For operations that don’t return data (like DELETE):
src/routes/tasks/tasks.routes.ts
export const remove = createRoute ({
path: "/tasks/{id}" ,
method: "delete" ,
request: {
params: IdParamsSchema ,
},
tags: [ "Tasks" ],
responses: {
[HttpStatusCodes. NO_CONTENT ]: {
description: "Task deleted" ,
},
[HttpStatusCodes. NOT_FOUND ]: jsonContent (
notFoundSchema ,
"Task not found" ,
),
},
});
Organizing Documentation
Group related endpoints using tags:
const tags = [ "Tasks" ];
export const list = createRoute ({
path: "/tasks" ,
method: "get" ,
tags , // Groups with other task endpoints
// ...
});
In Scalar, endpoints are organized by these tags in the sidebar.
Descriptions
Add descriptions to provide more context:
export const create = createRoute ({
path: "/tasks" ,
method: "post" ,
summary: "Create a new task" ,
description: "Creates a new task with the specified name and completion status." ,
request: {
body: jsonContentRequired (
insertTasksSchema ,
"The task to create" ,
),
},
tags: [ "Tasks" ],
responses: {
[HttpStatusCodes. OK ]: jsonContent (
selectTasksSchema ,
"The created task" ,
),
},
});
Schema Reuse
Zod schemas defined in your database layer are automatically converted to OpenAPI schemas:
export const tasks = sqliteTable ( "tasks" , {
id: integer ({ mode: "number" }). primaryKey ({ autoIncrement: true }),
name: text (). notNull (),
done: integer ({ mode: "boolean" }). notNull (). default ( false ),
});
export const selectTasksSchema = toZodV4SchemaTyped (
createSelectSchema ( tasks )
);
export const insertTasksSchema = toZodV4SchemaTyped (
createInsertSchema ( tasks , {
name : field => field . min ( 1 ). max ( 500 ),
}). omit ({ id: true })
);
These schemas are used in routes and automatically appear in OpenAPI:
{
"components" : {
"schemas" : {
"Task" : {
"type" : "object" ,
"properties" : {
"id" : { "type" : "number" },
"name" : {
"type" : "string" ,
"minLength" : 1 ,
"maxLength" : 500
},
"done" : { "type" : "boolean" }
},
"required" : [ "name" , "done" ]
}
}
}
}
Zod validation rules (like .min(), .max(), .email()) are automatically converted to OpenAPI constraints.
Interactive Documentation with Scalar
The /reference endpoint provides interactive API documentation:
Features
Try It Out Test endpoints directly from the browser with real requests
Request Examples View code examples in multiple languages (fetch, curl, etc.)
Schema Explorer Browse request and response schemas with examples
Dark Mode Beautiful UI with dark mode support (Kepler theme)
Scalar Configuration
Customize Scalar’s appearance and behavior:
src/lib/configure-open-api.ts
app . get ( "/reference" , Scalar ({
url: "/doc" ,
theme: "kepler" , // Theme: kepler, saturn, default, etc.
layout: "classic" , // Layout: classic, modern
defaultHttpClient: {
targetKey: "js" , // Default language for examples
clientKey: "fetch" , // Default HTTP client
},
searchHotKey: "k" , // Keyboard shortcut for search
showSidebar: true , // Show/hide sidebar
}));
kepler : Modern dark theme (default in template)
saturn : Light theme with dark mode option
default : Classic Scalar appearance
purple : Purple accent colors
bluePlanet : Blue-focused theme
Advanced OpenAPI Features
Custom OpenAPI Info
Add more metadata to your API:
app . doc ( "/doc" , {
openapi: "3.0.0" ,
info: {
version: packageJSON . version ,
title: "Tasks API" ,
description: "A production-ready API for managing tasks with full OpenAPI documentation" ,
contact: {
name: "API Support" ,
email: "[email protected] " ,
url: "https://example.com/support" ,
},
license: {
name: "MIT" ,
url: "https://opensource.org/licenses/MIT" ,
},
},
servers: [
{
url: "http://localhost:3000" ,
description: "Development server" ,
},
{
url: "https://api.example.com" ,
description: "Production server" ,
},
],
});
Security Schemes
Document authentication:
app . doc ( "/doc" , {
openapi: "3.0.0" ,
info: { /* ... */ },
components: {
securitySchemes: {
bearerAuth: {
type: "http" ,
scheme: "bearer" ,
bearerFormat: "JWT" ,
},
apiKey: {
type: "apiKey" ,
in: "header" ,
name: "X-API-Key" ,
},
},
},
security: [
{ bearerAuth: [] },
],
});
Then reference in routes:
export const create = createRoute ({
path: "/tasks" ,
method: "post" ,
security: [
{ bearerAuth: [] },
],
// ...
});
Accessing the OpenAPI Spec
JSON Endpoint
The raw spec is available at /doc:
curl http://localhost:3000/doc
{
"openapi" : "3.0.0" ,
"info" : {
"version" : "1.0.0" ,
"title" : "Tasks API"
},
"paths" : {
"/tasks" : { /* ... */ }
}
}
The OpenAPI spec can be used with:
Code generators : Generate clients in any language
Postman/Insomnia : Import as a collection
Testing tools : Generate test cases
Linters : Validate API design
# Generate a TypeScript client
npx openapi-typescript http://localhost:3000/doc -o api-types.ts
# Generate a Python client
openapi-generator-cli generate -i http://localhost:3000/doc -g python
Best Practices
Document All Responses Include success and all error responses (404, 422, etc.) so consumers know what to expect.
Use Descriptive Names Write clear descriptions for routes, parameters, and responses.
Group with Tags Organize endpoints into logical groups using tags.
Keep Schemas DRY Reuse Zod schemas from your database layer—don’t duplicate them.
Never manually edit the OpenAPI spec. Always update route definitions instead. The spec is generated automatically and manual changes will be overwritten.
Testing Your Documentation
Start your dev server:
Open the interactive docs:
http://localhost:3000/reference
View the raw OpenAPI spec:
http://localhost:3000/doc
Test endpoints directly from Scalar’s “Try It Out” feature
Regularly check /reference during development to ensure your API is documented as expected. It’s also a great way to manually test endpoints.
Next Steps
Architecture Learn about the overall project structure
Type Safety Understand end-to-end type safety