Hono is a lightweight web framework that runs on Cloudflare Workers, Deno Deploy, Vercel Edge, Bun, and Node.js — the same app.get(path, handler) API works everywhere. Middleware chains with app.use() for CORS, auth, logging, and rate limiting. @hono/zod-openapi generates OpenAPI specs from typed route definitions. Hono’s RPC mode exports a type-safe client — client.orders.$get({ query: { customerId } }) types the request and response from the server router. SSR with JSX renders HTML on the edge without a build step. hono/jsx/streaming streams responses incrementally. @hono/swagger-ui auto-generates API documentation. Claude Code generates Hono application routers, middleware, OpenAPI-typed routes, RPC client configurations, and the deployment configurations for production edge applications.
CLAUDE.md for Hono
## Hono Stack
- Version: hono >= 4.6, @hono/zod-openapi >= 0.16
- Routing: app.get/post/put/delete(path, ...handlers) — same API all runtimes
- Middleware: app.use("*", middleware) — runs before handlers
- Validation: @hono/zod-openapi for typed routes with OpenAPI output
- RPC: hc<AppType>(url) — typed client from router type
- JSX: import { jsx } from 'hono/jsx' or use jsx preset
- Context: c.req.json/param/query, c.json/text/html/notFound
- Deploy: wrangler for CF Workers, deno deploy for Deno, vercel for Edge
Core Application
// src/app.ts — Hono application with middleware
import { Hono } from "hono"
import { cors } from "hono/cors"
import { logger } from "hono/logger"
import { bearerAuth } from "hono/bearer-auth"
import { rateLimiter } from "hono-rate-limiter"
import { ordersRouter } from "./routes/orders"
import { productsRouter } from "./routes/products"
type Bindings = {
DB: D1Database // Cloudflare D1
CACHE: KVNamespace // Cloudflare KV
API_SECRET: string
}
type Variables = {
userId: string // Set by auth middleware
}
export const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
// Global middleware
app.use("*", logger())
app.use("*", cors({
origin: ["https://app.example.com"],
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Authorization", "Content-Type"],
credentials: true,
}))
app.use("*", async (c, next) => {
const start = Date.now()
await next()
c.res.headers.set("X-Response-Time", `${Date.now() - start}ms`)
})
// Auth middleware for /api routes
app.use("/api/*", async (c, next) => {
const auth = c.req.header("Authorization")
if (!auth?.startsWith("Bearer ")) {
return c.json({ error: "Unauthorized" }, 401)
}
const token = auth.slice(7)
// Verify JWT using Cloudflare Workers crypto
const userId = await verifyToken(token, c.env.API_SECRET)
if (!userId) return c.json({ error: "Invalid token" }, 401)
c.set("userId", userId)
await next()
})
// Mount sub-routers
app.route("/api/orders", ordersRouter)
app.route("/api/products", productsRouter)
// Health check
app.get("/health", c => c.json({ status: "ok", ts: Date.now() }))
export default app
OpenAPI Route Definitions
// src/routes/orders-openapi.ts — typed routes with OpenAPI
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"
import { swaggerUI } from "@hono/swagger-ui"
const OrderSchema = z.object({
id: z.string().openapi({ example: "ord-abc123" }),
status: z.enum(["pending", "processing", "shipped", "delivered", "cancelled"]),
totalCents: z.number().int().openapi({ example: 2999 }),
createdAt: z.string().datetime(),
}).openapi("Order")
const CreateOrderSchema = z.object({
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive(),
priceCents: z.number().int().positive(),
})).min(1),
}).openapi("CreateOrder")
const listOrdersRoute = createRoute({
method: "get",
path: "/",
request: {
query: z.object({
customerId: z.string().optional(),
status: z.string().optional(),
limit: z.coerce.number().int().max(100).default(20).optional(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({
orders: z.array(OrderSchema),
total: z.number(),
}),
},
},
description: "List of orders",
},
},
})
const createOrderRoute = createRoute({
method: "post",
path: "/",
request: {
body: {
content: {
"application/json": { schema: CreateOrderSchema },
},
},
},
responses: {
201: {
content: {
"application/json": { schema: OrderSchema },
},
description: "Created order",
},
422: {
content: {
"application/json": { schema: z.object({ error: z.string() }) },
},
description: "Validation error",
},
},
})
export const ordersOpenAPIRouter = new OpenAPIHono()
ordersOpenAPIRouter.openapi(listOrdersRoute, async (c) => {
const { customerId, status, limit } = c.req.valid("query")
const orders = await db.listOrders({ customerId, status, limit })
return c.json({ orders, total: orders.length })
})
ordersOpenAPIRouter.openapi(createOrderRoute, async (c) => {
const body = c.req.valid("json")
const order = await db.createOrder(body)
return c.json(order, 201)
})
// Swagger UI
ordersOpenAPIRouter.get("/ui", swaggerUI({ url: "/api/orders/spec" }))
// OpenAPI spec endpoint
ordersOpenAPIRouter.doc("/spec", {
openapi: "3.0.0",
info: { title: "Orders API", version: "1.0.0" },
})
RPC Client
// src/client.ts — type-safe Hono RPC client
import { hc } from "hono/client"
import type { AppType } from "./app"
// AppType is the typeof app — includes all routes and their types
export const client = hc<AppType>("https://api.example.com", {
headers: {
Authorization: `Bearer ${getAuthToken()}`,
},
})
// Usage — fully typed, no manual type assertions needed
async function fetchOrders(customerId: string) {
const response = await client.api.orders.$get({
query: { customerId, limit: "20" },
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
// Return type inferred from server handler
const { orders } = await response.json()
return orders
}
async function createOrder(items: OrderItem[]) {
const response = await client.api.orders.$post({
json: { items },
})
return response.json()
}
JSX Server Rendering
// src/routes/pages.tsx — Hono JSX for server-rendered HTML
import { Hono } from "hono"
import { html } from "hono/html"
const pages = new Hono()
// Hono JSX component
function OrderCard({ order }: { order: Order }) {
return (
<div class="order-card">
<h3>Order #{order.id.slice(-8)}</h3>
<span class={`status status--${order.status}`}>{order.status}</span>
<p>${(order.totalCents / 100).toFixed(2)}</p>
</div>
)
}
pages.get("/dashboard", async (c) => {
const userId = c.var.userId
const orders = await fetchOrders(userId)
return c.html(
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Dashboard</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<h1>Your Orders</h1>
<div class="orders-grid">
{orders.map(order => <OrderCard key={order.id} order={order} />)}
</div>
</body>
</html>
)
})
// Streaming SSR
pages.get("/dashboard/stream", async (c) => {
return c.streamText(async (stream) => {
await stream.writeln(
"<!DOCTYPE html><html><body><h1>Orders</h1>"
)
const orders = await fetchOrders(c.var.userId)
for (const order of orders) {
await stream.writeln(
`<div>${order.id} — ${order.status}</div>`
)
}
await stream.writeln("</body></html>")
})
})
export { pages }
Cloudflare Workers Deployment
# wrangler.toml
name = "orders-api"
main = "src/worker.ts"
compatibility_date = "2024-09-23"
[[d1_databases]]
binding = "DB"
database_name = "orders"
database_id = "xxxx-xxxx-xxxx"
[[kv_namespaces]]
binding = "CACHE"
id = "xxxx"
[vars]
ENVIRONMENT = "production"
// src/worker.ts — Cloudflare Worker entry point
import { app } from "./app"
export default app
For the Express.js alternative that’s better for traditional Node.js deployments where Cloudflare Workers compatibility isn’t needed and the large Express middleware ecosystem (passport.js, multer) is required, the Node.js API guide covers Express patterns. For the Fastify framework that provides better TypeScript support and performance than Express while remaining Node.js-native, see the Fastify guide for schema validation and plugin architecture. The Claude Skills 360 bundle includes Hono skill sets covering routing, middleware, OpenAPI, and edge deployment. Start with the free tier to try Hono application generation.