Skip to main contenthono-fieldguide
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.
跳到安装 用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/outfitter-dev/outfitter --skill hono-fieldguide命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
同仓库更多 Skills
Patterns for @outfitter/daemon including lifecycle management, IPC communication, health checks, and PID files. Use when building background services, daemons, or when "daemon", "IPC", "health check", "background service", or "@outfitter/daemon" are mentioned.
Creates handlers, CLI commands, MCP tools, and daemon services following Outfitter Dev Kit conventions. Use when adding new components to a project, scaffolding code, or when "create handler", "new command", "add tool", or "daemon service" are mentioned.
Deep patterns for @outfitter/mcp including tool registration, Zod schemas, resources, and server configuration. Use when building MCP servers, registering tools, defining resources, or when "MCP server", "MCP tool", "registerTool", or "@outfitter/mcp" are mentioned.
| 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..();
c.({ id });
})
.(, (c) => {
body = c..();
c.({ : }, );
});
= app;
req
param
"id"
return
json
post
"/users"
async
const
await
req
json
return
json
created
true
201
export
type
AppType
typeof
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 });
});
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: [] }));
Factory Pattern — Context Typing
Use createFactory<Env>() to type context variables across middleware and routes.
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.createHandlers((c) => {
const user = c.get("user");
return c.json({ user });
});
const app = factory
.createApp()
.use("*", dbMiddleware)
.use("/api/*", authMiddleware)
.get("/api/profile", ...getProfile);
export type AppType = typeof app;
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);
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 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));
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() }) },
},
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");
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);
});
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
app.doc("/openapi.json", {
openapi: "3.1.0",
info: { title: "API", version: "1.0.0" },
});
RPC Client — End-to-End Types
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.posts.$get();
const data = await res.json();
const res2 = await client.posts[":id"].$get({ param: { id: "123" } });
const res3 = await client.posts.$post({
json: { title: "Hello", content: "World" },
});
const res4 = await client.posts.$get(
{},
{
headers: { Authorization: "Bearer token" },
}
);
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", async () => {
const res = await client.posts.$post({
json: { title: "Test", content: "Content" },
});
expect(res.status).toBe(201);
const data = await res.json();
expect(data).toMatchObject({ title: "Test" });
});
test("Protected route requires auth", async () => {
const res = await client.api.profile.$get();
expect(res.status).toBe(401);
});
test("Protected route accepts valid token", async () => {
const res = await client.api.profile.$get(
{},
{
headers: { Authorization: "Bearer valid-token" },
}
);
expect(res.status).toBe(200);
});
});
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(async (c, next) => {
c.set("requestId", crypto.randomUUID());
await next();
c.res.headers.set("x-request-id", c.get("requestId"));
});
Rules
- 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
- 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
- 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