Skip to main content
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
{
  "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

Tags

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:
src/db/schema.ts
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": { /* ... */ }
  }
}

Using with Other Tools

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

  1. Start your dev server:
    npm run dev
    
  2. Open the interactive docs:
    http://localhost:3000/reference
    
  3. View the raw OpenAPI spec:
    http://localhost:3000/doc
    
  4. 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

Build docs developers (and LLMs) love