Hono
Hono is a lightweight web framework that runs on Cloudflare Workers, Deno, Bun, Node.js, and more. This guide shows how to serve an OpenAPI spec generated by to-openapi from a Hono application.
Basic Setup
Generate your spec with openapi() and serve it from a GET /openapi.json route.
ts
import { Hono } from "hono";
import { openapi } from "to-openapi";
import { z } from "zod";
const spec = openapi({
info: {
title: "Hono API",
version: "1.0.0",
},
servers: [{ url: "http://localhost:3000" }],
paths: {
"GET /users": {
summary: "List all users",
tags: ["Users"],
query: z.object({
limit: z.number().optional(),
offset: z.number().optional(),
}),
200: z.array(
z.object({
id: z.string(),
name: z.string(),
email: z.string(),
})
),
},
"POST /users": {
summary: "Create a user",
tags: ["Users"],
body: z.object({
name: z.string(),
email: z.string(),
}),
201: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
400: { description: "Validation error" },
},
"GET /users/{id}": {
summary: "Get a user by ID",
tags: ["Users"],
params: z.object({
id: z.string(),
}),
200: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
404: { description: "User not found" },
},
"DELETE /users/{id}": {
summary: "Delete a user",
tags: ["Users"],
params: z.object({
id: z.string(),
}),
204: { description: "User deleted" },
404: { description: "User not found" },
},
},
});
const app = new Hono();
// Serve the OpenAPI spec
app.get("/openapi.json", (c) => {
return c.json(spec);
});
// Implement your actual routes
app.get("/users", (c) => {
return c.json([]);
});
app.post("/users", async (c) => {
const body = await c.req.json();
return c.json({ id: "1", ...body }, 201);
});
app.get("/users/:id", (c) => {
const id = c.req.param("id");
return c.json({ id, name: "Alice", email: "alice@example.com" });
});
app.delete("/users/:id", (c) => {
return c.body(null, 204);
});
export default app;Using the Class-Based API
The OpenAPI class builder lets you add routes incrementally, which pairs well with Hono's routing style.
ts
import { Hono } from "hono";
import { OpenAPI } from "to-openapi";
import { z } from "zod";
const api = new OpenAPI({
info: {
title: "Hono API",
version: "1.0.0",
},
servers: [{ url: "http://localhost:3000" }],
});
const app = new Hono();
// Define a route and register it with the OpenAPI builder
api.route("get", "/posts", {
summary: "List posts",
tags: ["Posts"],
200: z.array(
z.object({
id: z.number(),
title: z.string(),
body: z.string(),
})
),
});
app.get("/posts", (c) => {
return c.json([]);
});
api.route("post", "/posts", {
summary: "Create a post",
tags: ["Posts"],
body: z.object({
title: z.string(),
body: z.string(),
}),
201: z.object({
id: z.number(),
title: z.string(),
body: z.string(),
}),
});
app.post("/posts", async (c) => {
const data = await c.req.json();
return c.json({ id: 1, ...data }, 201);
});
// Serve the generated spec
app.get("/openapi.json", (c) => {
return c.json(api.document());
});
export default app;Hono's Built-in OpenAPI Support
Hono has its own OpenAPI integration through @hono/zod-openapi, which tightly couples route definitions with Zod schemas and validation.
Use @hono/zod-openapi when:
- You want validation and spec generation in one package.
- Your project exclusively uses Zod.
Use to-openapi when:
- You use multiple schema libraries (Zod, ArkType, Valibot, or any Standard Schema provider).
- You need the plugin system for cross-cutting concerns like bearer auth or error responses.
- You want to merge specs from multiple services using
merge(). - You prefer to keep spec generation separate from your routing logic.