mcpField
H
hono-api-best-practices

hono-api-best-practices

github.com/marcioaltoe开发069 次安装今天更新
代码仓库

关于 hono-api-best-practices

hono-api-best-practices 是一项为 Hono 与 Zod 后端设计、审查和规范 API 的技能,面向构建 TypeScript 后端及其客户端契约的开发者。它要求先确定 REST 或仅 POST 的风格,并围绕 Zod 契约和生成的 OpenAPI 文档组织流程。

主要功能

  1. 01选择 REST 或仅 POST 风格
  2. 02以 Zod 定义 API 契约
  3. 03通过路由生成 OpenAPI
  4. 04统一错误映射与响应校验
  5. 05规范认证、版本和限流

适用场景

  1. 01设计新的 HTTP 端点
  2. 02审查现有 API 路由
  3. 03为客户端或 MCP 消费者定义契约
  4. 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.md
openapi-generation.md
style-post-only.md
style-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

按以下顺序确定风格:

  1. 以项目文档为准。 检查 AGENTS.md、ADR 和设计文档。如果其中指定了某种风格(或与此技能中的任何内容冲突),则以项目文档为准。
  2. 根据现有路由推断。 搜索 createRoute({ 并检查 method / path:动词多样且路径包含 /{id} 表示 REST;每条路由的 method 均为 "post" 且以 /<action> 结尾,则表示仅 POST 风格。
  3. 新项目或情况不明确: 询问用户项目使用哪种风格。除非项目已统一采用仅 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。

设计工作流(自上而下)

  1. 从用例入手
    • 确定有界上下文和用例(例如 ListCustomers、CreateOrder)。
    • 在应用层定义清晰的输入/输出 DTO。
  2. 映射到选定风格
    • 应用风格参考文件中的路径、方法、参数和响应约定。
    • 确定状态码和错误结构。
  3. 映射 HTTP ↔ 用例
    • 路由/控制器将经过验证的 HTTP 输入解析为应用 DTO,调用用例,并将结果映射回 HTTP。
  4. 一致地应用横切关注点
    • 身份验证、授权、验证、日志记录、幂等性、速率限制和错误处理均遵循共享辅助函数。

审计工作流

审计现有路由或 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 令牌属于单独的凭证类型。通过 OpenAPI security 声明,以便代码生成/MCP 工具能够对其建模。
  • 授权: 不在访问范围内的资源 → 404;资源存在但无权访问 → 403;查询筛选条件只能缩小凭证的边界,不能扩大边界。
  • 字段格式: JSON 字段/查询参数使用 camelCase;金额使用十进制字符串 + ISO 4217(绝不使用浮点数);时间点使用带时区的 ISO 8601 格式,业务日期使用 YYYY-MM-DD;时间筛选条件应明确指定维度(使用 createdFrom,而不是 from)。
  • 异步操作: 返回 202 Accepted 和包含明确状态枚举的状态资源,并通过 GET 轮询。

状态码

  • 200 OK — 读取或更新成功,且包含响应体
  • 201 Created — 创建成功;条件允许时应包含 Location
  • 202 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

不可妥协的规则:

  1. 每个复用的架构都必须调用 .openapi("RefName"),以便将其包含在 components.schemas 中。
  2. 每个端点都必须通过 createRoute 声明,并使用 app.openapi(route, handler) 挂载。
  3. responses 中必须列出端点可能返回的每个响应状态码。
  4. 必须通过 OpenAPI security 声明身份验证,并在引导阶段使用 registerComponent("securitySchemes", ...) 注册一次。
  5. 不要提交生成的 OpenAPI 规范。

如需查看完整操作指南(架构注解、引导配置、客户端代码生成、常见错误),请阅读 references/openapi-generation.md。如需查看符合你所用风格的路由声明示例,请阅读该风格的参考文件。

测试策略

  • 契约:使用有效和无效载荷对 Zod 架构进行单元测试。
  • 控制器:对 Hono 路由进行集成测试,涵盖状态码、身份验证、校验、错误映射,以及符合所用风格的响应结构。
  • OpenAPI:启动服务,在测试中获取 /openapi.json,并验证端点、方法、架构、响应码和安全配置。
  • 端到端:通过公共 API 或生成的客户端测试关键流程。

API 设计检查清单

合并 API 变更前:

  1. 端点始终遵循项目选定的风格(不得混用 REST 和仅 POST 风格)。
  2. 契约定义在共享包中,并使用 .openapi("RefName") 注解。
  3. 路由使用 createRoute,并通过 app.openapi 挂载;声明每个成功和错误状态码,以及正确的 security 配置。
  4. 参数位置符合所用风格(REST:路径/查询/正文;仅 POST:仅 JSON 正文)。
  5. 响应结构符合所用风格(REST:直接返回资源;仅 POST:使用 { data, message } 信封)。错误响应使用 { error, details: { code } }。
  6. 控制器保持精简、自行注册,并委托给用例。
  7. 类型化应用错误通过共享 HTTP 错误辅助函数进行映射。
  8. 在 OpenAPI 中以机器可读形式声明身份验证和授权,并在代码中强制执行。
  9. 启动时重新生成 /openapi.json 并体现变更;通过获取规范进行验证。
  10. 测试涵盖成功、校验失败、身份验证失败、授权失败和领域冲突。
  11. 处理横切关注点:不安全的操作(创建/状态变更)通过 Idempotency-Key 实现幂等;明确说明版本策略及仅追加/破坏性变更规则;声明速率限制标头(RateLimit-*/Retry-After)。对于只读端点,应注明幂等性不适用,而不是省略。
  12. 遵循共享约定(见 references/api-conventions.md):凭据声明 scopes + accessBoundary;正确使用 403 与 404 进行授权,且过滤条件绝不扩大范围;字段格式遵循 camelCase/金额使用十进制字符串 + ISO 4217/ISO 8601 时刻/YYYY-MM-DD 日期/命名的时间过滤器;异步操作返回 202 + 状态资源。

易踩的问题(两种风格均适用)

  • 不要盲目照搬表名;路径代表面向用户的产品资源,而不是数据库架构。
  • 不要使用 app.get(...) / app.post(...) 绕过 OpenAPI 路由注册。
  • 不要在自由格式的 description 文本中隐藏身份验证要求;请使用 security。
  • 不要在不进行版本管理的情况下对响应做出破坏性变更。
  • 不要内联错误响应;请通过共享辅助函数处理。

各风格特定的命名规则和易踩的问题见各自的参考文件。

评论

支持 Markdown