- 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>
```typescript
// ✅ Chained routes preserve types
const app = new Hono()
.get("/users", (c) => c.json({ users: [] }))
.get("/users/:id", (c) => {
const id = c.req.param("id"); // Typed!
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; // Full route types!
```
**❌ NEVER break the chain:**
```typescript
const app = new Hono();
app.get("/users", handler1); // Types LOST!
app.post("/users", handler2);
```
**Path parameters** — typed automatically:
```typescript
.get('/posts/:id/comments/:commentId', (c) => {
const { id, commentId } = c.req.param(); // Both string
return c.json({ postId: id, commentId });
})
```
**Query parameters** — use Zod for validation:
```typescript
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"); // Fully typed!
return c.json({ page, limit });
});
```
**Middleware in chain:**
```typescript
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>
```typescript
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>();
// Typed middleware
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); // Type-checked!
await next();
});
// Typed handlers
const getProfile = factory.createHandlers((c) => {
const user = c.get("user"); // Typed: { id: string; role: 'admin' | 'user' }
return c.json({ user });
});
// Assemble app
const app = factory
.createApp()
.use("*", dbMiddleware)
.use("/api/*", authMiddleware)
.get("/api/profile", ...getProfile);
export type AppType = typeof app;
```
**Multi-module structure:**
```typescript
// routes/users.ts
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);
});
// index.ts
const app = factory
.createApp()
.use("*", dbMiddleware)
.route("/users", usersRoute)
.route("/posts", postsRoute);
```
See [factory-pattern.md](references/factory-pattern.md) for advanced patterns.
</factory_pattern>
## Error Handling
<error_handling>
```typescript
import { HTTPException } from "hono/http-exception";
// Throw typed errors
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 });
});
// Custom error classes
class NotFoundError extends HTTPException {
constructor(resource: string) {
super(404, { message: `${resource} not found` });
}
}
class UnauthorizedError extends HTTPException {
constructor(message = "Unauthorized") {
super(401, { message });
}
}
// Centralized handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
if (err instanceof ZodError) {
return c.json(
{
error: "Validation failed",
issues: err.issues.map((i) => ({
path: i.path.join("."),
message: i.message,
})),
},
400
);
}
const isDev = Bun.env.NODE_ENV !== "production";
return c.json({ error: isDev ? err.message : "Internal server error" }, 500);
});
app.notFound((c) => c.json({ error: "Not found", path: c.req.path }, 404));
```
See [error-handling.md](references/error-handling.md) for patterns.
</error_handling>
## Zod OpenAPI
<zod_openapi>
```typescript
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() }) },
},
description: "User not found",
},
},
tags: ["Users"],
summary: "Get user by ID",
});
const app = new OpenAPIHono();
app.openapi(route, (c) => {
const { id } = c.req.valid("param"); // Typed!
const user = db.query("SELECT * FROM users WHERE id = ?").get(id);
if (!user) return c.json({ error: "User not found" }, 404);
return c.json(user, 200);
});
// Swagger UI
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
app.doc("/openapi.json", {
openapi: "3.1.0",
info: { title: "API", version: "1.0.0" },
});
```
See [zod-openapi.md](references/zod-openapi.md) for complete patterns.
</zod_openapi>
## RPC Client — End-to-End Types
<rpc_client>
```typescript
// Server
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;
// Client
import { hc } from "hono/client";
import type { AppType } from "./server";
const client = hc<AppType>("http://localhost:3000");
// GET request
const res = await client.posts.$get();
const data = await res.json(); // Typed: { posts: any[] }
// GET with params
const res2 = await client.posts[":id"].$get({ param: { id: "123" } });
// POST request
const res3 = await client.posts.$post({
json: { title: "Hello", content: "World" },
});
// With headers
const res4 = await client.posts.$get(
{},
{
headers: { Authorization: "Bearer token" },
}
);
```
</rpc_client>
## Testing with testClient
<testing>
```typescript
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", async () => {
const res = await client.posts.$post({
在 GitHub 查看