hono-api-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

API Design & Standards (Hono + Zod)

API设计与标准(Hono + Zod)

When to Use This Skill

何时使用此技能

  • Defining new HTTP endpoints for a bounded context
  • Reviewing, auditing, or refactoring existing API routes for consistency
  • Establishing API standards for a TypeScript/Clean Architecture backend
  • Designing contracts for frontend, SDK, CLI, MCP, or third-party consumers
  • Defining or updating API contracts in a shared contracts package (e.g.
    api-contracts
    )
  • Enforcing consistent route registration and generated OpenAPI docs
  • 为限界上下文定义新的HTTP端点
  • 审查、审核或重构现有API路由以保证一致性
  • 为TypeScript/Clean Architecture后端建立API标准
  • 为前端、SDK、CLI、MCP或第三方消费者设计契约
  • 在共享契约包(如
    api-contracts
    )中定义或更新API契约
  • 强制一致的路由注册和生成的OpenAPI文档

Choose the API Style First

先选择API风格

This skill supports two mutually exclusive conventions. Lock onto exactly one per project before designing or auditing any endpoint, and never mix them within the same API surface. The shared core below (Clean Architecture, Zod as source of truth,
createRoute
+
app.openapi
, the error helper, generated OpenAPI) applies to both styles. Only paths, methods, parameter placement, and response shapes differ.
StyleShapeRead
Standard RESTResource paths + HTTP verbs (
GET
/
POST
/
PATCH
/
PUT
/
DELETE
); params in path/query/body; responses return the resource/collection directly
references/style-rest.md
POST-only action-basedEvery endpoint is
POST /<context>/<entity>/<action>
; JSON body only; responses use the
{ data, message }
/
{ error, details }
envelope
references/style-post-only.md
Determine the style in this order:
  1. Project docs win. Check
    AGENTS.md
    , ADRs, and design docs. If they specify a style (or conflict with anything in this skill), the project documents take precedence.
  2. Infer from existing routes. Grep for
    createRoute({
    and inspect
    method
    /
    path
    : varied verbs with
    /{id}
    paths means REST; every route
    method: "post"
    with
    /<action>
    suffixes means POST-only.
  3. Greenfield or ambiguous: ask the user which style the project uses. Default to standard REST for new public APIs unless the project has already standardized on POST-only.
Once chosen, read only that style's reference file and apply it consistently. If a project already uses one style, do not introduce endpoints in the other.
此技能支持两种互斥的约定。在设计或审核任何端点之前,每个项目必须锁定其中一种,且绝不能在同一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
    、架构决策记录(ADRs)和设计文档。如果它们指定了风格(或与本技能内容冲突),则以项目文档为准。
  2. 从现有路由推断。搜索
    createRoute({
    并检查
    method
    /
    path
    :使用多种动词且路径包含
    /{id}
    表示REST;所有路由均为
    method: "post"
    且路径带有
    /<action>
    后缀表示仅POST。
  3. 新项目或不明确场景:询问用户项目使用哪种风格。对于新的公共API,默认使用标准REST,除非项目已统一采用仅POST风格。
选择后,仅阅读该风格的参考文件并始终如一地应用。如果项目已使用一种风格,则不要引入另一种风格的端点。

Core Principles (both styles)

核心原则(两种风格通用)

  • Contracts in a shared
    api-contracts
    package
    : Request/response schemas live in a central package when shared across backend, frontend, SDKs, or tests.
  • Zod is the single source of truth: Every schema drives runtime validation, TypeScript types (
    z.infer
    ), and the OpenAPI spec at once. Never duplicate hand-written OpenAPI YAML/JSON.
  • Every endpoint uses
    createRoute
    +
    app.openapi(route, handler)
    : Never register handlers directly with
    app.get(...)
    ,
    app.post(...)
    , etc., or the endpoint vanishes from
    /openapi.json
    . The
    createRoute
    call must list every possible response status code in
    responses
    .
  • Error mapping goes through a single shared helper: Typed application errors map to HTTP in one place (e.g.
    mapApplicationErrorToResponse(context, error)
    ). Never inline
    return context.json({ error, details: { code } }, status)
    in a controller, use case, or route handler. If the helper doesn't cover a new error type, extend the helper — do not branch inline.
  • openapi.json
    is generated, never authored
    : The spec is produced by
    @hono/zod-openapi
    from registered schemas and routes. If the spec is wrong, fix the Zod schema, not the spec. Never commit a generated spec.
  • Scalar UI consumes the generated spec: Interactive docs render from the same
    /openapi.json
    the clients consume. No separate documentation artifact exists.
  • Clean Architecture: Controllers are thin adapters over application use cases. Design starts from domain use cases and application DTOs, then maps to HTTP.
  • 契约存放在共享
    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
    渲染。不存在单独的文档工件。
  • Clean Architecture:控制器是应用用例之上的轻量适配器。设计从领域用例和应用DTO开始,然后映射到HTTP。

Design Workflow (Top-Down)

设计工作流(自上而下)

  1. Start from the use case
    • Identify the bounded context and use case (e.g.
      ListCustomers
      ,
      CreateOrder
      ).
    • Define clear input/output DTOs in the application layer.
  2. Map to the chosen style
    • Apply the path, method, parameter, and response conventions from your style's reference file.
    • Decide status codes and error shapes.
  3. Map HTTP ↔ use case
    • The route/controller parses validated HTTP input into application DTOs, calls the use case, and maps the result back to HTTP.
  4. Apply cross-cutting concerns consistently
    • Auth, authorization, validation, logging, idempotency, rate limits, and error handling follow shared helpers.
  1. 从用例开始
    • 识别限界上下文和用例(例如
      ListCustomers
      CreateOrder
      )。
    • 在应用层定义清晰的输入/输出DTO。
  2. 映射到所选风格
    • 应用所选风格参考文件中的路径、方法、参数和响应约定。
    • 确定状态码和错误格式。
  3. 映射HTTP ↔ 用例
    • 路由/控制器将经过验证的HTTP输入解析为应用DTO,调用用例,并将结果映射回HTTP。
  4. 始终如一地应用横切关注点
    • 认证、授权、验证、日志、幂等性、速率限制和错误处理遵循共享辅助工具。

Audit Workflow

审核工作流

When auditing an existing route or API surface, first confirm the project's style, then walk the §API Design Checklist for each endpoint under review and report pass/fail per item — including schema-level concerns like
.openapi("RefName")
metadata on every request/response schema, not just method/path. A route that uses the right method and shape but inlines Zod schemas (missing
.openapi("RefName")
) is still non-compliant and must be flagged. So is any endpoint that uses the wrong style for the project.
审核现有路由或API范围时,首先确认项目的风格,然后针对每个待审核端点逐一检查§API设计检查表的各项——包括模式级别的关注点,如每个请求/响应模式上的
.openapi("RefName")
元数据,而不仅仅是方法/路径。即使路由使用了正确的方法和格式,但内联了Zod模式(缺少
.openapi("RefName")
),仍不符合规范,必须标记出来。使用与项目风格不符的端点也是如此。

Validate Both Directions (request AND response)

双向验证(请求和响应)

createRoute
+
app.openapi
validate the request (params, query, body) at runtime, but they do not validate the response — Hono sends whatever the handler returns, even if it drifts from the declared response schema. Declaring response schemas in
responses
and asserting them in tests is not the same as guaranteeing them in production. Validate the outgoing body at runtime too, so the API can never silently return an off-contract response that breaks generated SDKs/MCP/CLI tools.
Do this through one shared response helper that parses the payload through the response schema before sending. A mismatch throws and is caught by the global error handler (mapped to
500
) rather than shipping a malformed body:
ts
// 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
}
Keep
schema.parse
always-on in non-production and at least sampled in production if the parse cost matters; never skip it entirely, or the response contract is unenforced. Request-side: register a
defaultHook
on the
OpenAPIHono
instance so failed request validation becomes the shared error envelope (
400
) instead of Hono's default body — see
references/openapi-generation.md
.
createRoute
+
app.openapi
在运行时验证请求(参数、查询、请求体),但它们不验证响应——Hono会发送处理器返回的任何内容,即使它偏离了声明的响应模式。在
responses
中声明响应模式并在测试中断言它们,并不等同于在生产环境中保证它们。也要在运行时验证输出的请求体,这样API就永远不会静默返回不符合契约的响应,从而破坏生成的SDK/MCP/CLI工具。
通过一个共享的响应辅助工具实现这一点,该工具在发送前通过响应模式解析负载。不匹配会抛出错误并被全局错误处理器捕获(映射为
500
),而不是发送格式错误的请求体:
ts
// src/infra/http/respond.ts  — 共享工具,两种风格的所有控制器均使用
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); // 根据契约进行运行时验证
}
在非生产环境中始终启用
schema.parse
;如果解析成本重要,在生产环境中至少进行抽样检查;绝不要完全跳过它,否则响应契约将无法强制执行。请求端:在
OpenAPIHono
实例上注册
defaultHook
,使失败的请求验证变为共享错误信封(
400
),而非Hono的默认请求体——请参阅
references/openapi-generation.md

Controller Pattern (both styles)

控制器模式(两种风格通用)

Controllers self-register routes via
app.openapi(route, handler)
.
createRoute
validates the request — no separate
zValidator
call is needed. On success the controller returns through
respond(...)
so the body is validated against the declared response schema; the success shape differs by style (the resource schema directly for REST, the
{ data, message }
response schema for POST-only). The error path is identical and goes through the shared error helper.
ts
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);
    });
  }
}
See your style's reference file for the exact success shape and concrete
createRoute
examples.
控制器通过
app.openapi(route, handler)
自行注册路由。
createRoute
会验证请求——无需单独调用
zValidator
。成功时,控制器通过
respond(...)
返回,以便请求体根据声明的响应模式进行验证;成功格式因风格而异(REST直接返回资源模式,仅POST返回
{ data, message }
响应模式)。错误路径相同,均通过共享错误辅助工具处理。
ts
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"); // 或 "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: respond(context, SomeResponseSchema, { data: result.value, message: "Success" }, 200)
      return respond(context, SomeResponseSchema, result.value, 200);
    });
  }
}
有关确切的成功格式和具体的
createRoute
示例,请参阅所选风格的参考文件。

Cross-Cutting Concerns (both styles)

横切关注点(两种风格通用)

Bake these into the contract, not just the prose — generated SDK/MCP/CLI consumers depend on them being machine-readable.
  • Idempotency for unsafe operations. Creation and state-changing operations must be safely retryable — clients and gateways retry on timeouts, and a naive retry double-charges or double-creates. Accept an
    Idempotency-Key
    (REST header, or an
    idempotencyKey
    body field in POST-only) on create/state-change endpoints; persist the key with its first result for a dedup window; a replay with the same key returns the original response, the same key with a different payload returns
    409
    . Reads are naturally idempotent and need no key.
  • Versioning & breaking changes. Version the surface from day one (
    /v1
    prefix for REST; a version segment or header for POST-only). Within a version make only additive, backward-compatible changes: add optional fields, never remove or repurpose existing ones, never tighten validation on existing inputs. A breaking change requires a new version plus a deprecation path — signal removal with
    Deprecation
    and
    Sunset
    response headers and a migration window. Otherwise generated consumers break silently.
  • Rate limiting. Declare
    429 Too Many Requests
    and make limits observable: emit
    RateLimit-Limit
    ,
    RateLimit-Remaining
    , and
    RateLimit-Reset
    on responses, plus
    Retry-After
    on a
    429
    , and declare them in the route's
    responses
    headers so codegen clients can back off. Scope limits per credential/workspace, not per IP, for authenticated APIs.
将这些融入契约,而不仅仅是文字说明——生成的SDK/MCP/CLI消费者依赖于它们的机器可读性。
  • 不安全操作的幂等性。创建和状态变更操作必须能够安全重试——客户端和网关会在超时后重试,简单的重试可能会导致重复收费或重复创建。在创建/状态变更端点接受
    Idempotency-Key
    (REST为头信息,仅POST为请求体中的
    idempotencyKey
    字段);在去重窗口内保留密钥及其首次结果;使用相同密钥的重试返回原始响应,使用相同密钥但不同负载的重试返回
    409
    。读取操作天然具有幂等性,无需密钥。
  • 版本控制与破坏性变更。从第一天开始对API范围进行版本控制(REST使用
    /v1
    前缀;仅POST使用版本段或头信息)。在同一版本内仅进行可向后兼容的增量变更:添加可选字段,绝不要删除或重新使用现有字段,绝不要收紧对现有输入的验证。破坏性变更需要新版本加上弃用路径——使用
    Deprecation
    Sunset
    响应头信号移除,并提供迁移窗口。否则生成的消费者会静默崩溃。
  • 速率限制。声明
    429 Too Many Requests
    并使限制可观测:在响应中返回
    RateLimit-Limit
    RateLimit-Remaining
    RateLimit-Reset
    ,在
    429
    响应中添加
    Retry-After
    ,并在路由的
    responses
    头中声明它们,以便代码生成客户端可以退避。对于已认证的API,按凭证/工作区而非IP设置限制范围。

Shared Conventions

共享约定

Credential model, authorization semantics, and field formats apply to both styles and are detailed in
references/api-conventions.md
— read it when shaping contracts, credentials, or money/date fields. Key defaults:
  • Credentials carry
    scopes
    (what) +
    accessBoundary
    (where).
    Never infer broad access; whole-tenant access needs an explicit boundary. MCP tokens are a separate credential type. Declare via OpenAPI
    security
    so codegen/MCP tooling can model them.
  • Authorization: out-of-scope resource →
    404
    ; exists-but-forbidden →
    403
    ; query filters narrow but never expand a credential's boundary.
  • Field formats: camelCase JSON fields/query params; money as a decimal string + ISO 4217 (never floats); instants ISO 8601 with timezone, business dates
    YYYY-MM-DD
    ; temporal filters name their dimension (
    createdFrom
    , not
    from
    ).
  • Async operations:
    202 Accepted
    + a status resource with an explicit state enum, polled via
    GET
    .
凭证模型、授权语义和字段格式适用于两种风格,详情请参阅
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
    轮询。

Status Codes

状态码

  • 200 OK
    — successful reads and updates with a body
  • 201 Created
    — successful creation; include
    Location
    when practical
  • 202 Accepted
    — async job accepted (REST resource-style operations)
  • 204 No Content
    — successful delete/revoke/disconnect with no body
  • 304 Not Modified
    — conditional GET with unchanged representation (REST)
  • 400 Bad Request
    — malformed input or validation error at the HTTP boundary
  • 401 Unauthorized
    — missing/invalid authentication
  • 403 Forbidden
    — authenticated but not allowed
  • 404 Not Found
    — resource not found
  • 409 Conflict
    — duplicates, state conflicts, business invariants
  • 422 Unprocessable Entity
    — domain validation failure distinct from HTTP validation
  • 429 Too Many Requests
    — rate limit
  • 500 Internal Server Error
    — unhandled errors
  • 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 Envelope (both styles)

错误信封(两种风格通用)

Errors use the same structured body in both styles.
error
is a human-readable string safe for UI; everything machine-readable lives inside
details
.
json
{
  "error": "Human-readable message",
  "details": {
    "code": "RESOURCE_NOT_FOUND",
    "requestId": "req_01HZ..."
  }
}
Rules:
  • error
    — always a string; never an object, never holds the code.
  • details.code
    always required; stable, machine-readable,
    SCREAMING_SNAKE_CASE
    .
  • details.requestId
    — required in production for traceability.
  • Additional optional context goes inside
    details
    :
    fieldErrors
    ,
    resourceId
    ,
    retryAfter
    , etc.
  • Never put
    code
    at the top level, never nest
    { error: { code, message } }
    , never return free-form error strings.
两种风格的错误均使用相同的结构化请求体。
error
是适合UI显示的人类可读字符串;所有机器可读内容都在
details
内。
json
{
  "error": "人类可读消息",
  "details": {
    "code": "RESOURCE_NOT_FOUND",
    "requestId": "req_01HZ..."
  }
}
规则:
  • error
    — 始终为字符串;绝不要是对象,绝不要包含错误码。
  • details.code
    必须始终存在;稳定、机器可读、采用大写下划线格式(SCREAMING_SNAKE_CASE)。
  • details.requestId
    — 生产环境中必须存在,用于可追溯性。
  • 额外的可选上下文放在
    details
    内:
    fieldErrors
    resourceId
    retryAfter
    等。
  • 绝不要将
    code
    放在顶层,绝不要嵌套
    { error: { code, message } }
    ,绝不要返回自由格式的错误字符串。

OpenAPI Spec Generated From Zod

从Zod生成OpenAPI规范

The OpenAPI specification is a build artifact of the Zod schemas, never hand-written.
text
Zod schema  →  .openapi("RefName")  →  createRoute({ ... })
            →  app.openapi(route, handler)  →  /openapi.json
            →  Scalar UI  +  generated clients
Non-negotiable rules:
  1. Every reused schema must call
    .openapi("RefName")
    so it appears in
    components.schemas
    .
  2. Every endpoint must be declared with
    createRoute
    and mounted with
    app.openapi(route, handler)
    .
  3. Every response status code the endpoint can emit must be listed in
    responses
    .
  4. Auth must be declared via OpenAPI
    security
    , registered once at bootstrap with
    registerComponent("securitySchemes", ...)
    .
  5. Do not commit generated OpenAPI specs.
For the full walkthrough (schema annotation, bootstrap wiring, client codegen, common mistakes), read
references/openapi-generation.md
. For the route-declaration examples in your convention, read your style's reference file.
OpenAPI规范是Zod模式的构建工件,绝不是手写的。
text
Zod schema  →  .openapi("RefName")  →  createRoute({ ... })
            →  app.openapi(route, handler)  →  /openapi.json
            →  Scalar UI  +  生成的客户端
不可协商的规则:
  1. 每个可复用的模式必须调用
    .openapi("RefName")
    ,使其出现在
    components.schemas
    中。
  2. 每个端点必须用
    createRoute
    声明,并通过
    app.openapi(route, handler)
    挂载。
  3. 端点可能返回的每个响应状态码必须在
    responses
    中列出。
  4. 认证必须通过OpenAPI
    security
    声明,在启动时通过
    registerComponent("securitySchemes", ...)
    注册一次。
  5. 不要提交生成的OpenAPI规范。
有关完整的步骤(模式注解、启动配置、客户端代码生成、常见错误),请阅读
references/openapi-generation.md
。有关所选约定的路由声明示例,请阅读对应风格的参考文件。

Testing Strategy

测试策略

  • Contracts: unit test Zod schemas with valid/invalid payloads.
  • Controllers: integration test Hono routes — status codes, auth, validation, error mapping, and the response shape for your style.
  • OpenAPI: boot the server, fetch
    /openapi.json
    in tests, and verify endpoints, methods, schemas, response codes, and security.
  • E2E: exercise key flows through the public API or a generated client.
  • 契约:使用有效/无效负载对Zod模式进行单元测试。
  • 控制器:对Hono路由进行集成测试——状态码、认证、验证、错误映射以及对应风格的响应格式。
  • OpenAPI:启动服务器,在测试中获取
    /openapi.json
    ,验证端点、方法、模式、响应码和安全设置。
  • 端到端(E2E):通过公共API或生成的客户端测试关键流程。

API Design Checklist

API设计检查表

Before merging an API change:
  1. The endpoint follows the project's chosen style consistently (no mixing REST and POST-only).
  2. Contracts are defined in the shared package and annotated with
    .openapi("RefName")
    .
  3. Route uses
    createRoute
    and is mounted with
    app.openapi
    , declaring every success and error status code plus the correct
    security
    entry.
  4. Parameter placement matches the style (REST: path/query/body; POST-only: JSON body only).
  5. Response shape matches the style (REST: resource directly; POST-only:
    { data, message }
    envelope). Error responses use
    { error, details: { code } }
    .
  6. Controller is thin, self-registering, and delegates to a use case.
  7. Typed application errors map through the shared HTTP error helper.
  8. Auth and authorization are machine-readable in OpenAPI and enforced in code.
  9. /openapi.json
    regenerates at boot and reflects the change — verified by fetching the spec.
  10. Tests cover success, validation failure, auth failure, authorization failure, and domain conflicts.
  11. Cross-cutting concerns are addressed: unsafe (create/state-change) operations are idempotent via
    Idempotency-Key
    ; the versioning posture is stated with additive-only/breaking-change rules; rate-limit headers (
    RateLimit-*
    /
    Retry-After
    ) are declared. For a read-only endpoint, note idempotency as N/A rather than omitting it.
  12. Shared conventions hold (see
    references/api-conventions.md
    ): credentials declare
    scopes
    +
    accessBoundary
    ; authorization uses
    403
    vs
    404
    correctly and filters never expand scope; field formats follow camelCase / money-as-decimal-string + ISO 4217 / ISO 8601 instants /
    YYYY-MM-DD
    dates / named temporal filters; async operations return
    202
    + a status resource.
合并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
    vs
    404
    ,过滤器从不扩大权限范围;字段格式遵循驼峰式 / 金额为十进制字符串 + ISO 4217 / ISO 8601时间戳 /
    YYYY-MM-DD
    日期 / 命名时间过滤器;异步操作返回
    202
    + 状态资源。

Pitfalls (both styles)

常见陷阱(两种风格通用)

  • Do not mirror table names blindly; paths represent public product resources, not the database schema.
  • Do not bypass OpenAPI route registration with
    app.get(...)
    /
    app.post(...)
    .
  • Do not hide auth requirements in free-form
    description
    text; use
    security
    .
  • Do not make breaking response changes without versioning.
  • Do not inline error responses; route them through the shared helper.
Style-specific naming rules and pitfalls live in each style's reference file.
  • 不要盲目镜像表名;路径代表公共产品资源,而非数据库模式。
  • 不要使用
    app.get(...)
    /
    app.post(...)
    绕过OpenAPI路由注册。
  • 不要将认证要求隐藏在自由格式的
    description
    文本中;使用
    security
  • 不要在不进行版本控制的情况下对响应进行破坏性变更。
  • 不要内联错误响应;通过共享辅助工具处理。
特定风格的命名规则和陷阱请参阅各风格的参考文件。