| name | hono-fieldguide |
| description | This skill should be used when building APIs with Hono, using hc client, implementing OpenAPI, or when Hono, RPC, or type-safe API are mentioned. |
| metadata | {"version":"1.0.1"} |
Hono API Development
Route chaining → type-safe RPC → end-to-end types.
<when_to_use>
- Building REST APIs with Hono
- Type-safe RPC with hono/client
- OpenAPI documentation with Zod
- Testing APIs with testClient
- When user mentions "Hono", "RPC", or "OpenAPI"
NOT for: Bun runtime APIs (use bun-fieldguide), other frameworks (Express, Fastify)
</when_to_use>
<version_notes>
Hono v4+ with @hono/zod-openapi v1.0+
Check hono.dev for latest patterns.
</version_notes>
Route Chaining — Critical Pattern
Type inference flows through method chain. Break chain = lose types.
<route_chaining>
const app = new Hono()
.get("/users", (c) => c.json({ users: [] }))
.get("/users/:id", (c) => {
const id = c.req.param("id");
return c.json({ id });
})
.post("/users", async (c) => {
const body = await c.req.json();
return c.json({ created: true }, 201);
});
export type AppType = typeof app;
❌ NEVER break the chain:
const app = new Hono();
app.get("/users", handler1);
app.post("/users", handler2);
Path parameters — typed automatically:
.get('/posts/:id/comments/:commentId', (c) => {
const { id, commentId } = c.req.param();
return c.json({ postId: id, commentId });
})
Query parameters — use Zod for validation:
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
});
const app = new Hono().get("/search", zValidator("query", QuerySchema), (c) => {
const { page, limit } = c.req.valid("query");
return c.json({ page, limit });
});
Middleware in chain:
const app = new Hono()
.use("*", logger())
.use("/api/*", cors())
.get("/api/public", (c) => c.json({ public: true }))
.use("/api/admin/*", authMiddleware)
.get("/api/admin/users", (c) => c.json({ users: [] }));
</route_chaining>
Factory Pattern — Context Typing
Use createFactory<Env>() to type context variables across middleware and routes.
<factory_pattern>
import { createFactory } from "hono/factory";
import type { Database } from "bun:sqlite";
type Env = {
Variables: {
user: { id: string; role: "admin" | "user" };
requestId: string;
db: Database;
};
};
const factory = createFactory<Env>();
const authMiddleware = factory.createMiddleware(async (c, next) => {
const token = c.req.header("authorization")?.replace("Bearer ", "");
if (!token) throw new HTTPException(401, { message: "Unauthorized" });
const user = await verifyToken(token);
c.set("user", user);
await next();
});
const getProfile = factory.( {
user = c.();
c.({ user });
});
app = factory
.()
.(, dbMiddleware)
.(, authMiddleware)
.(, ...getProfile);
= app;
Multi-module structure:
export const usersRoute = factory
.createApp()
.get("/", (c) => c.json({ users: [] }))
.post("/", zValidator("json", CreateUserSchema), async (c) => {
const data = c.req.valid("json");
return c.json({ created: true }, 201);
});
const app = factory
.createApp()
.use("*", dbMiddleware)
.route("/users", usersRoute)
.route("/posts", postsRoute);
See factory-pattern.md for advanced patterns.
</factory_pattern>
Error Handling
<error_handling>
import { HTTPException } from "hono/http-exception";
app.get("/users/:id", async (c) => {
const user = await findUser(c.req.param("id"));
if (!user) {
throw new HTTPException(404, { message: "User not found" });
}
return c.json({ user });
});
class NotFoundError extends HTTPException {
constructor(resource: string) {
super(404, { message: `${resource} not found` });
}
}
class UnauthorizedError extends HTTPException {
constructor(message = "Unauthorized") {
super(401, { message });
}
}
app.onError((err, c) => {
if (err ) {
c.({ : err. }, err.);
}
(err ) {
c.(
{
: ,
: err..( ({
: i..(),
: i.,
})),
},
);
}
isDev = .. !== ;
c.({ : isDev ? err. : }, );
});
app.( c.({ : , : c.. }, ));
See error-handling.md for patterns.
</error_handling>
Zod OpenAPI
<zod_openapi>
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { swaggerUI } from "@hono/swagger-ui";
const UserSchema = z
.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
})
.openapi("User");
const route = createRoute({
method: "get",
path: "/users/{id}",
request: {
params: z.object({ id: z.string().uuid() }),
},
responses: {
200: {
content: { "application/json": { schema: UserSchema } },
description: "User found",
},
404: {
content: {
"application/json": { schema: z.object({ error: z.string() }) },
},
: ,
},
},
: [],
: ,
});
app = ();
app.(route, {
{ id } = c..();
user = db.().(id);
(!user) c.({ : }, );
c.(user, );
});
app.(, ({ : }));
app.(, {
: ,
: { : , : },
});
See zod-openapi.md for complete patterns.
</zod_openapi>
RPC Client — End-to-End Types
<rpc_client>
const app = new Hono()
.get("/posts", (c) => c.json({ posts: [] }))
.get("/posts/:id", (c) => c.json({ id: c.req.param("id") }))
.post("/posts", zValidator("json", CreatePostSchema), async (c) => {
const data = c.req.valid("json");
return c.json({ id: "123", ...data }, 201);
});
export type AppType = typeof app;
import { hc } from "hono/client";
import type { AppType } from "./server";
const client = hc<AppType>("http://localhost:3000");
const res = await client..$get();
data = res.();
res2 = client.[].$get({ : { : } });
res3 = client..$post({
: { : , : },
});
res4 = client..$get(
{},
{
: { : },
}
);
</rpc_client>
Testing with testClient
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { testClient } from "hono/testing";
import { Database } from "bun:sqlite";
import app from "./server";
describe("API Tests", () => {
let db: Database;
beforeEach(() => {
db = new Database(":memory:");
db.run(
"CREATE TABLE posts (id TEXT PRIMARY KEY, title TEXT, content TEXT)"
);
});
afterEach(() => {
db.close();
});
const client = testClient(app);
test("GET /posts returns posts", async () => {
const res = await client.posts.$get();
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveProperty("posts");
});
test("POST /posts creates post", () => {
res = client..$post({
: { : , : },
});
(res.).();
data = res.();
(data).({ : });
});
(, () => {
res = client...$get();
(res.).();
});
(, () => {
res = client...$get(
{},
{
: { : },
}
);
(res.).();
});
});
See testing-patterns.md for complete patterns.
Middleware Patterns
import { logger } from "hono/logger";
app.use("*", logger());
import { cors } from "hono/cors";
app.use(
"/api/*",
cors({
origin: ["http://localhost:3000"],
credentials: true,
})
);
const rateLimiter = factory.createMiddleware(async (c, next) => {
const ip = c.req.header("x-forwarded-for") || "unknown";
const key = `rate:${ip}`;
const count = await cache.incr(key);
if (count === 1) await cache.expire(key, 60);
if (count > 100) {
throw new HTTPException(429, { message: "Rate limit exceeded" });
}
await next();
});
const requestId = factory.createMiddleware( (c, next) => {
c.(, crypto.());
();
c...(, c.());
});
Rules
ALWAYS:
- Chain routes for type inference →
.get().post().put()
- Export
type AppType = typeof app for RPC client
- Use
createFactory<Env>() for typed context variables
- Validate with Zod schemas via
zValidator
- Handle errors with
HTTPException and centralized onError
- Test with
testClient for type safety
NEVER:
- Break method chain with variable assignment between routes
- Use
any types — let Hono infer or define explicitly
- Use
JSON.parse(await c.req.text()) — use c.req.json() or Zod validator
- Skip request validation on user input
- Expose stack traces in production
When Type Errors Occur:
- Check route chaining not broken
- Verify
export type AppType matches actual app
- Ensure middleware uses
createFactory for context types
- Check client using correct
param, query, or json keys
References
Examples:
References:
External: