关于 hono-api-best-practices
hono-api-best-practices 是一项为 Hono 与 Zod 后端设计、审查和规范 API 的技能,面向构建 TypeScript 后端及其客户端契约的开发者。它要求先确定 REST 或仅 POST 的风格,并围绕 Zod 契约和生成的 OpenAPI 文档组织流程。
主要功能
- 01选择 REST 或仅 POST 风格
- 02以 Zod 定义 API 契约
- 03通过路由生成 OpenAPI
- 04统一错误映射与响应校验
- 05规范认证、版本和限流
适用场景
- 01设计新的 HTTP 端点
- 02审查现有 API 路由
- 03为客户端或 MCP 消费者定义契约
- 04统一 OpenAPI 文档生成
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ marcioaltoe/ skills --skill hono-api-best-practices安装后在 Claude Code 中输入/hono-api-best-practices确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-conventions.mdopenapi-generation.mdstyle-post-only.mdstyle-rest.md详细介绍
API 设计与规范(Hono + Zod)
何时使用此技能
- 为有界上下文定义新的 HTTP 端点
- 为确保一致性而审查、审计或重构现有 API 路由
- 为 TypeScript/Clean Architecture 后端制定 API 规范
- 为前端、SDK、CLI、MCP 或第三方使用者设计契约
- 在共享契约包(例如
api-contracts)中定义或更新 API 契约 - 确保路由注册方式一致,并生成 OpenAPI 文档
首先选择 API 风格
此技能支持两种互斥的约定。在设计或审计任何端点之前,每个项目必须确定且仅确定一种风格,并且绝不要在同一个 API 范围内混用。以下共享核心(Clean Architecture、作为事实来源的 Zod、createRoute + app.openapi、错误辅助函数、生成的 OpenAPI)适用于两种风格。只有路径、方法、参数位置和响应格式有所不同。
| 风格 | 形式 | 阅读 |
|---|---|---|
| 标准 REST | 资源路径 + HTTP 动词(GET/POST/PATCH/PUT/DELETE);参数位于路径/查询/请求体中;响应直接返回资源/集合 | references/style-rest.md |
| 仅 POST 的基于操作的风格 | 每个端点均为 POST /<context>/<entity>/<action>;仅使用 JSON 请求体;响应采用 { data, message } / { error, details } 信封格式 | references/style-post-only.md |
按以下顺序确定风格:
- 以项目文档为准。 检查
AGENTS.md、ADR 和设计文档。如果其中指定了某种风格(或与此技能中的任何内容冲突),则以项目文档为准。 - 根据现有路由推断。 搜索
createRoute({并检查method/path:动词多样且路径包含/{id}表示 REST;每条路由的method均为"post"且以/<action>结尾,则表示仅 POST 风格。 - 新项目或情况不明确: 询问用户项目使用哪种风格。除非项目已统一采用仅 POST 风格,否则新建公开 API 时默认采用标准 REST。
选定风格后,只阅读该风格对应的参考文件,并始终遵循该风格。如果项目已经采用一种风格,就不要引入另一种风格的端点。
核心原则(两种风格通用)
- 在共享的
api-contracts包中定义契约:当后端、前端、SDK 或测试需要共享请求/响应模式时,将其放在中央包中。 - Zod 是唯一事实来源:每个模式都同时用于运行时验证、TypeScript 类型(
z.infer)和 OpenAPI 规范。切勿重复编写 OpenAPI YAML/JSON。 - 每个端点都使用
createRoute+app.openapi(route, handler):切勿直接使用app.get(...)、app.post(...)等注册处理程序,否则该端点不会出现在/openapi.json中。createRoute调用必须在responses中列出所有可能的响应状态码。 - 错误映射统一通过一个共享辅助函数处理:在同一处将带类型的应用错误映射为 HTTP 响应(例如
mapApplicationErrorToResponse(context, error))。切勿在控制器、用例或路由处理程序中内联return context.json({ error, details: { code } }, status)。如果辅助函数尚未覆盖新的错误类型,就扩展该辅助函数,不要内联分支处理。 openapi.json由程序生成,绝不手动编写:规范由@hono/zod-openapi根据已注册的模式和路由生成。如果规范有误,应修正 Zod 模式,而不是规范。切勿提交生成的规范文件。- Scalar UI 使用生成的规范:交互式文档根据客户端使用的同一份
/openapi.json呈现。不存在单独的文档产物。 - 整洁架构:控制器是应用用例之上的轻量适配器。设计从领域用例和应用 DTO 入手,然后映射到 HTTP。
设计工作流(自上而下)
- 从用例入手
- 确定有界上下文和用例(例如
ListCustomers、CreateOrder)。 - 在应用层定义清晰的输入/输出 DTO。
- 确定有界上下文和用例(例如
- 映射到选定风格
- 应用风格参考文件中的路径、方法、参数和响应约定。
- 确定状态码和错误结构。
- 映射 HTTP ↔ 用例
- 路由/控制器将经过验证的 HTTP 输入解析为应用 DTO,调用用例,并将结果映射回 HTTP。
- 一致地应用横切关注点
- 身份验证、授权、验证、日志记录、幂等性、速率限制和错误处理均遵循共享辅助函数。
审计工作流
审计现有路由或 API 接口时,首先确认项目采用的风格,然后针对每个待审查的端点逐项检查 §API 设计检查清单,并逐项报告通过/未通过情况;检查内容包括模式级问题,例如每个请求/响应模式上的 .openapi("RefName") 元数据,而不只是方法/路径。即使路由使用了正确的方法和结构,只要内联定义了 Zod 模式(缺少 .openapi("RefName")),仍然不符合要求,必须指出。端点采用了与项目不符的风格,也必须指出。
验证两个方向(请求和响应)
createRoute + app.openapi 会在运行时验证请求(参数、查询、正文),但不会验证响应——Hono 会发送处理程序返回的任何内容,即使它与声明的响应模式不一致。在 responses 中声明响应模式并在测试中断言,并不等同于在生产环境中保证响应符合模式。也要在运行时验证输出正文,确保 API 不会悄然返回不符合契约的响应,导致生成的 SDK/MCP/CLI 工具出错。
应通过一个共享响应辅助函数处理:发送前先使用响应模式解析负载。不匹配时会抛出错误,并由全局错误处理程序捕获(映射为 500),而不是发送格式错误的正文:
// src/infra/http/respond.ts — shared, used by every controller in both styles
import type { Context } from "hono";
import type { ContentfulStatusCode } from "hono/utils/http-status";
import type { z } from "@hono/zod-openapi";
export function respond<S extends z.ZodTypeAny>(
context: Context,
schema: S,
payload: z.input<S>,
status: ContentfulStatusCode
) {
return context.json(schema.parse(payload), status); // runtime-validated against the contract
}
在非生产环境中始终执行 schema.parse;如果解析成本较高,生产环境中也至少应进行抽样验证。切勿完全跳过,否则响应契约将得不到强制执行。请求侧:在 OpenAPIHono 实例上注册 defaultHook,使请求验证失败时返回共享错误封套(400),而不是 Hono 的默认正文——请参阅 references/openapi-generation.md。
控制器模式(两种风格均适用)
控制器通过 app.openapi(route, handler) 自行注册路由。createRoute 会验证请求,无需单独调用 zValidator。成功时,控制器通过 respond(...) 返回响应,因此响应体会根据声明的响应架构进行验证;成功响应的结构因风格而异(REST 直接使用资源架构,纯 POST 风格使用 { data, message } 响应架构)。错误处理路径相同,都会经过共享的错误辅助函数。
import type { OpenAPIHono } from "@hono/zod-openapi";
import { someRoute } from "../routes/some.route";
import { SomeResponseSchema } from "@/api-contracts";
import { mapApplicationErrorToResponse } from "@/infra/http/error-handling";
import { respond } from "@/infra/http/respond";
export class SomeController {
constructor(
private readonly app: OpenAPIHono,
private readonly useCase: SomeUseCase
) {
this.app.openapi(someRoute, async context => {
const input = context.req.valid("json"); // or "query" / "param"
const credential = context.get("credential");
const result = await this.useCase.execute({ ...input, workspaceId: credential.workspaceId });
if (!result.ok) return mapApplicationErrorToResponse(context, result.error);
// REST: respond(context, SomeResourceSchema, result.value, 200)
// POST-only: respond(context, SomeResponseSchema, { data: result.value, message: "Success" }, 200)
return respond(context, SomeResponseSchema, result.value, 200);
});
}
}
请参阅对应风格的参考文件,了解确切的成功响应结构和具体的 createRoute 示例。
横切关注点(两种风格均适用)
应将以下内容纳入契约,而不只是写在说明文字中,因为生成的 SDK/MCP/CLI 消费方依赖这些内容以机器可读的形式提供。
- 不安全操作的幂等性。 创建和状态变更操作必须支持安全重试——客户端和网关会在超时时重试,简单重试可能导致重复扣款或重复创建。在创建/状态变更端点接受
Idempotency-Key(REST 使用标头,纯 POST 风格在请求体中使用idempotencyKey字段);在去重时间窗口内,将键与首次请求的结果一起保存;使用相同键重放请求时返回原始响应,同一键对应不同负载时返回409。读取操作天然具有幂等性,无需使用键。 - 版本控制与破坏性变更。 从一开始就为接口设置版本(REST 使用
/v1前缀;纯 POST 风格使用版本路径段或标头)。在同一版本内只进行增量式、向后兼容的变更:添加可选字段,不要删除或改变现有字段的用途,也不要收紧现有输入的验证规则。破坏性变更需要使用新版本,并提供弃用路径——通过Deprecation和Sunset响应标头提示移除,并留出迁移窗口。否则,生成的消费方可能会在无提示的情况下中断。 - 速率限制。 声明
429 Too Many Requests,并确保限制可观测:在响应中发送RateLimit-Limit、RateLimit-Remaining和RateLimit-Reset,在429响应中添加Retry-After,并在路由的responses标头中声明这些内容,以便代码生成客户端能够进行退避。对于经过身份验证的 API,应按凭证/工作区而非 IP 限制请求速率。
共享约定
凭证模型、授权语义和字段格式适用于两种风格,详见 references/api-conventions.md——设计契约、凭证或金额/日期字段时请阅读该文件。主要默认约定:
- 凭证包含
scopes(权限范围)和accessBoundary(访问边界)。 切勿推断凭证具有广泛访问权限;访问整个租户必须有明确的边界。MCP 令牌属于单独的凭证类型。通过 OpenAPIsecurity声明,以便代码生成/MCP 工具能够对其建模。 - 授权: 不在访问范围内的资源 →
404;资源存在但无权访问 →403;查询筛选条件只能缩小凭证的边界,不能扩大边界。 - 字段格式: JSON 字段/查询参数使用 camelCase;金额使用十进制字符串 + ISO 4217(绝不使用浮点数);时间点使用带时区的 ISO 8601 格式,业务日期使用
YYYY-MM-DD;时间筛选条件应明确指定维度(使用createdFrom,而不是from)。 - 异步操作: 返回
202 Accepted和包含明确状态枚举的状态资源,并通过GET轮询。
状态码
200 OK— 读取或更新成功,且包含响应体201 Created— 创建成功;条件允许时应包含Location202 Accepted— 已接受异步任务(REST 资源风格操作)204 No Content— 删除/撤销/断开连接成功,且无响应体304 Not Modified— 条件 GET 请求的表示内容未发生变化(REST)400 Bad Request— HTTP 边界处的输入格式错误或验证错误401 Unauthorized— 缺少身份验证信息或身份验证无效403 Forbidden— 已通过身份验证,但无权执行操作404 Not Found— 未找到资源409 Conflict— 重复项、状态冲突或业务约束冲突422 Unprocessable Entity— 与 HTTP 验证不同的领域验证失败429 Too Many Requests— 速率限制500 Internal Server Error— 未处理的错误
错误信封(两种风格均适用)
两种风格的错误都使用相同的结构化正文。error 是适合在 UI 中显示的人类可读字符串;所有机器可读内容都放在 details 中。
{
"error": "Human-readable message",
"details": {
"code": "RESOURCE_NOT_FOUND",
"requestId": "req_01HZ..."
}
}
规则:
error— 始终为字符串;绝不能是对象,也不能包含错误代码。details.code— 始终必填;稳定且机器可读,采用SCREAMING_SNAKE_CASE格式。details.requestId— 生产环境中必填,以便追踪。- 其他可选上下文放在
details中:fieldErrors、resourceId、retryAfter等。 - 绝不在顶层放置
code,绝不嵌套{ error: { code, message } },也绝不返回自由格式的错误字符串。
从 Zod 生成 OpenAPI 规范
OpenAPI 规范是由 Zod 架构生成的构建产物,绝不能手动编写。
Zod schema → .openapi("RefName") → createRoute({ ... })
→ app.openapi(route, handler) → /openapi.json
→ Scalar UI + generated clients
不可妥协的规则:
- 每个复用的架构都必须调用
.openapi("RefName"),以便将其包含在components.schemas中。 - 每个端点都必须通过
createRoute声明,并使用app.openapi(route, handler)挂载。 responses中必须列出端点可能返回的每个响应状态码。- 必须通过 OpenAPI
security声明身份验证,并在引导阶段使用registerComponent("securitySchemes", ...)注册一次。 - 不要提交生成的 OpenAPI 规范。
如需查看完整操作指南(架构注解、引导配置、客户端代码生成、常见错误),请阅读 references/openapi-generation.md。如需查看符合你所用风格的路由声明示例,请阅读该风格的参考文件。
测试策略
- 契约:使用有效和无效载荷对 Zod 架构进行单元测试。
- 控制器:对 Hono 路由进行集成测试,涵盖状态码、身份验证、校验、错误映射,以及符合所用风格的响应结构。
- OpenAPI:启动服务,在测试中获取
/openapi.json,并验证端点、方法、架构、响应码和安全配置。 - 端到端:通过公共 API 或生成的客户端测试关键流程。
API 设计检查清单
合并 API 变更前:
- 端点始终遵循项目选定的风格(不得混用 REST 和仅 POST 风格)。
- 契约定义在共享包中,并使用
.openapi("RefName")注解。 - 路由使用
createRoute,并通过app.openapi挂载;声明每个成功和错误状态码,以及正确的security配置。 - 参数位置符合所用风格(REST:路径/查询/正文;仅 POST:仅 JSON 正文)。
- 响应结构符合所用风格(REST:直接返回资源;仅 POST:使用
{ data, message }信封)。错误响应使用{ error, details: { code } }。 - 控制器保持精简、自行注册,并委托给用例。
- 类型化应用错误通过共享 HTTP 错误辅助函数进行映射。
- 在 OpenAPI 中以机器可读形式声明身份验证和授权,并在代码中强制执行。
- 启动时重新生成
/openapi.json并体现变更;通过获取规范进行验证。 - 测试涵盖成功、校验失败、身份验证失败、授权失败和领域冲突。
- 处理横切关注点:不安全的操作(创建/状态变更)通过
Idempotency-Key实现幂等;明确说明版本策略及仅追加/破坏性变更规则;声明速率限制标头(RateLimit-*/Retry-After)。对于只读端点,应注明幂等性不适用,而不是省略。 - 遵循共享约定(见
references/api-conventions.md):凭据声明scopes+accessBoundary;正确使用403与404进行授权,且过滤条件绝不扩大范围;字段格式遵循 camelCase/金额使用十进制字符串 + ISO 4217/ISO 8601 时刻/YYYY-MM-DD日期/命名的时间过滤器;异步操作返回202+ 状态资源。
易踩的问题(两种风格均适用)
- 不要盲目照搬表名;路径代表面向用户的产品资源,而不是数据库架构。
- 不要使用
app.get(...)/app.post(...)绕过 OpenAPI 路由注册。 - 不要在自由格式的
description文本中隐藏身份验证要求;请使用security。 - 不要在不进行版本管理的情况下对响应做出破坏性变更。
- 不要内联错误响应;请通过共享辅助函数处理。
各风格特定的命名规则和易踩的问题见各自的参考文件。