hono
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesehono
hono
方針
方针
HTTP フレームワークはプロジェクト内で Hono に統一する。同一プロジェクトに複数の HTTP フレームワークを混在させない。
ルート定義とスキーマと OpenAPI 文書を1か所から生成する。手書きの OpenAPI YAML を実装と別に持たない。二重管理は必ずずれる。
Hono は更新が速い。API の形やミドルウェアの引数に迷ったら、記憶に頼らず下のドキュメントを引く。
- ドキュメント https://hono.dev/docs/
- Hono オブジェクトとルーティング https://hono.dev/docs/api/hono
- HTTPException https://hono.dev/docs/api/exception
- ミドルウェアの仕組みと順序 https://hono.dev/docs/guides/middleware
- バリデーション https://hono.dev/docs/guides/validation
- bearer-auth https://hono.dev/docs/middleware/builtin/bearer-auth
- cors https://hono.dev/docs/middleware/builtin/cors
- secure-headers https://hono.dev/docs/middleware/builtin/secure-headers
- zod-openapi の例 https://hono.dev/examples/zod-openapi
- @hono/zod-openapi https://github.com/honojs/middleware/tree/main/packages/zod-openapi
- @hono/mcp https://github.com/honojs/middleware/tree/main/packages/mcp
- リリースと破壊的変更 https://github.com/honojs/hono/releases
仕様の一次ソース。
- RFC 9457 Problem Details https://www.rfc-editor.org/rfc/rfc9457.html
- Health Check Response Format https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check
在项目中统一使用 Hono 作为 HTTP 框架。不要在同一项目中混用多个 HTTP 框架。
从同一处生成路由定义、schema 和 OpenAPI 文档。不要在实现之外单独维护手写的 OpenAPI YAML。双重管理必然会产生偏差。
Hono 更新很快。如果对 API 形态或中间件参数不确定,不要依赖记忆,而应查阅以下文档。
- 文档 https://hono.dev/docs/
- Hono 对象与路由 https://hono.dev/docs/api/hono
- HTTPException https://hono.dev/docs/api/exception
- 中间件机制与顺序 https://hono.dev/docs/guides/middleware
- 验证 https://hono.dev/docs/guides/validation
- bearer-auth https://hono.dev/docs/middleware/builtin/bearer-auth
- cors https://hono.dev/docs/middleware/builtin/cors
- secure-headers https://hono.dev/docs/middleware/builtin/secure-headers
- zod-openapi 示例 https://hono.dev/examples/zod-openapi
- @hono/zod-openapi https://github.com/honojs/middleware/tree/main/packages/zod-openapi
- @hono/mcp https://github.com/honojs/middleware/tree/main/packages/mcp
- 版本发布与破坏性变更 https://github.com/honojs/hono/releases
以下是一手规范来源。
- RFC 9457 Problem Details https://www.rfc-editor.org/rfc/rfc9457.html
- 健康检查响应格式 https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check
URL 設計
URL 设计
- パスバージョニングを使う。/api/v1/ 配下に置く。
- パスはできる限り短くする。やむを得ず複数語になる場合は kebab-case。
- 名詞は単数形。/users ではなく /user。
- パスパラメータは /user/{id} の形式。OpenAPI の表記と揃える。
ts
import { Hono } from 'hono'
const api = new Hono().basePath('/api/v1')- 使用路径版本控制,并置于 /api/v1/ 下。
- 路径尽可能简短。不得不使用多个词时,采用 kebab-case。
- 名词使用单数形式,例如使用 /user,而不是 /users。
- 路径参数采用 /user/{id} 格式,并与 OpenAPI 中的写法保持一致。
ts
import { Hono } from 'hono'
const api = new Hono().basePath('/api/v1')OpenAPI
OpenAPI
@hono/zod-openapi を使い、ルートとスキーマから OpenAPI 文書を生成する。
ts
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const UserSchema = z.object({
id: z.string().openapi({ example: '01JABC' }),
displayName: z.string().max(50),
}).openapi('User')
const ProblemSchema = z.object({
type: z.string(),
title: z.string(),
status: z.number(),
detail: z.string().optional(),
}).openapi('Problem')
const getUser = createRoute({
method: 'get',
path: '/user/{id}',
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
content: { 'application/json': { schema: UserSchema } },
description: 'ユーザーを返す',
},
404: {
content: { 'application/problem+json': { schema: ProblemSchema } },
description: '該当するユーザーがない',
},
},
})
const app = new OpenAPIHono().basePath('/api/v1')
app.openapi(getUser, (c) => {
const { id } = c.req.valid('param')
return c.json({ id, displayName: '例' })
})
app.doc('/openapi.json', {
openapi: '3.1.0',
info: { title: 'Example API', version: '1.0.0' },
})- 成功と失敗の両方を responses に書く。エラー応答を省略しない。
- 入力は c.req.valid() で受ける。生の c.req.json() を検証なしで使わない。
- スキーマに .openapi('User') で名前を付け、コンポーネントとして再利用する。
使用 @hono/zod-openapi,从路由和 schema 生成 OpenAPI 文档。
ts
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const UserSchema = z.object({
id: z.string().openapi({ example: '01JABC' }),
displayName: z.string().max(50),
}).openapi('User')
const ProblemSchema = z.object({
type: z.string(),
title: z.string(),
status: z.number(),
detail: z.string().optional(),
}).openapi('Problem')
const getUser = createRoute({
method: 'get',
path: '/user/{id}',
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
content: { 'application/json': { schema: UserSchema } },
description: 'ユーザーを返す',
},
404: {
content: { 'application/problem+json': { schema: ProblemSchema } },
description: '該当するユーザーがない',
},
},
})
const app = new OpenAPIHono().basePath('/api/v1')
app.openapi(getUser, (c) => {
const { id } = c.req.valid('param')
return c.json({ id, displayName: '例' })
})
app.doc('/openapi.json', {
openapi: '3.1.0',
info: { title: 'Example API', version: '1.0.0' },
})- 在 responses 中同时编写成功和失败响应。不要省略错误响应。
- 输入通过 c.req.valid() 获取。不要在没有验证的情况下直接使用原始的 c.req.json()。
- 使用 .openapi('User') 为 schema 命名,并将其作为 component 复用。
エラー
错误
RFC 9457 の Problem Details を返す。Content-Type は application/problem+json。
ts
import { HTTPException } from 'hono/http-exception'
app.onError((err, c) => {
const status = err instanceof HTTPException ? err.status : 500
const problem = {
type: `https://example.com/problems/${status === 404 ? 'not-found' : 'internal'}`,
title: status === 404 ? 'Resource not found' : 'Internal server error',
status,
instance: c.get('requestId'),
}
return c.body(JSON.stringify(problem), status, {
'Content-Type': 'application/problem+json; charset=utf-8',
})
})- 本文の status は HTTP ステータス行と必ず一致させる。
- detail はデバッグ情報ではなく、クライアントが直すための説明にする。
- スタックトレース、内部ホスト名、DB 名、ユーザー情報を出さない。
- 未知のパスは app.notFound() で同じ形式の 404 を返す。SPA のフォールバックで 200 を返さない。errorpage スキルを参照。
- クライアントには文字列ではなく HTTP ステータスと安定した type URI で分岐させる。
返回符合 RFC 9457 的 Problem Details。Content-Type 为 application/problem+json。
ts
import { HTTPException } from 'hono/http-exception'
app.onError((err, c) => {
const status = err instanceof HTTPException ? err.status : 500
const problem = {
type: `https://example.com/problems/${status === 404 ? 'not-found' : 'internal'}`,
title: status === 404 ? 'Resource not found' : 'Internal server error',
status,
instance: c.get('requestId'),
}
return c.body(JSON.stringify(problem), status, {
'Content-Type': 'application/problem+json; charset=utf-8',
})
})- 响应正文中的 status 必须与 HTTP 状态行保持一致。
- detail 不应包含调试信息,而应说明客户端可以如何修正问题。
- 不要泄露堆栈跟踪、内部主机名、数据库名称或用户信息。
- 对于未知路径,使用 app.notFound() 返回相同格式的 404。不要通过 SPA fallback 返回 200。请参阅 errorpage skill。
- 客户端应根据 HTTP 状态和稳定的 type URI 进行分支处理,而不是依赖字符串。
認証
认证
ネットワークに公開する API は Bearer 認証。
ts
import { bearerAuth } from 'hono/bearer-auth'
app.use('/api/v1/*', bearerAuth({ verifyToken: async (token, c) => verify(token) }))loopback(127.0.0.1)にのみバインドするローカルサービスは、Bearer に代えて Origin と Host ヘッダーの検証で保護してよい。DNS リバインディング対策として必要。
ts
app.use('*', async (c, next) => {
const origin = c.req.header('Origin')
const host = c.req.header('Host')
if (origin && !ALLOWED_ORIGINS.includes(origin)) return c.body(null, 403)
if (!host || !/^(127\.0\.0\.1|\[::1\]|localhost)(:\d+)?$/.test(host)) return c.body(null, 403)
await next()
})- 認可はミドルウェアで一括せず、リソースの所有権をハンドラかサービス層で必ず確認する。
- トークン検証、所有権、レート制限をクライアント側の入力に委ねない。
对公开到网络中的 API 使用 Bearer 认证。
ts
import { bearerAuth } from 'hono/bearer-auth'
app.use('/api/v1/*', bearerAuth({ verifyToken: async (token, c) => verify(token) }))仅绑定到 loopback(127.0.0.1)的本地服务,可以使用 Origin 和 Host 标头验证代替 Bearer,以防范 DNS rebinding。
ts
app.use('*', async (c, next) => {
const origin = c.req.header('Origin')
const host = c.req.header('Host')
if (origin && !ALLOWED_ORIGINS.includes(origin)) return c.body(null, 403)
if (!host || !/^(127\.0\.0\.1|\[::1\]|localhost)(:\d+)?$/.test(host)) return c.body(null, 403)
await next()
})- 不要在中间件中统一处理授权;务必在 handler 或 service 层检查资源所有权。
- 不要依赖客户端输入来完成 token 验证、所有权检查和速率限制。
ヘルスチェック
健康检查
draft-inadarei-api-health-check に準拠させる。Content-Type は application/health+json。
ts
app.get('/health', (c) =>
c.body(JSON.stringify({
status: 'pass',
version: '1',
releaseId: process.env.RELEASE_ID,
checks: {
'db:responseTime': [{ componentType: 'datastore', status: 'pass' }],
},
}), 200, { 'Content-Type': 'application/health+json; charset=utf-8' }))- status は pass、warn、fail のいずれか。
- 認証の背後に置かない。監視から到達できなくなる。
- 依存先の詳細な内部情報を出さない。公開範囲を決めてから checks の粒度を決める。
遵循 draft-inadarei-api-health-check。Content-Type 为 application/health+json。
ts
app.get('/health', (c) =>
c.body(JSON.stringify({
status: 'pass',
version: '1',
releaseId: process.env.RELEASE_ID,
checks: {
'db:responseTime': [{ componentType: 'datastore', status: 'pass' }],
},
}), 200, { 'Content-Type': 'application/health+json; charset=utf-8' }))- status 只能是 pass、warn 或 fail 之一。
- 不要将其放在认证之后,否则监控将无法访问。
- 不要暴露依赖项的详细内部信息。先确定公开范围,再决定 checks 的粒度。
ミドルウェアの順序
中间件顺序
上から順に適用される。順序を誤ると認証前にハンドラが動く。
- requestId、logger。
- secureHeaders、cors。
- bearerAuth などの認証。
- ルート個別のバリデーション。
- ハンドラ。
- onError と notFound は最後に登録する。
cors は必要なオリジンだけを列挙する。ワイルドカードと credentials を併用しない。
按从上到下的顺序应用。顺序错误可能导致 handler 在认证之前执行。
- requestId、logger。
- secureHeaders、cors。
- bearerAuth 等认证。
- 路由级验证。
- handler。
- 最后注册 onError 和 notFound。
cors 仅列出必要的 origin。不要同时使用通配符和 credentials。
MCP サーバー
MCP 服务器
MCP サーバーを立てるなら、採用フレームワークに対応するアダプタを使う。Hono では @hono/mcp。別の HTTP サーバーを併設しない。
如果要部署 MCP 服务器,请使用与所选框架对应的 adapter。在 Hono 中使用 @hono/mcp。不要并存部署另一个 HTTP 服务器。
リント
Lint
sh
npx --yes @stoplight/spectral-cli lint openapi.json.spectral.yamlextends: ["spectral:oas"]sh
npx --yes @stoplight/spectral-cli lint openapi.json在 中设置 。对生成的 OpenAPI 文档运行该命令,并集成到 CI 中。不要通过禁用规则来消音,而应从设计层面修正问题。
.spectral.yamlextends: ["spectral:oas"]確定前に確認する
完成前确认
- HTTP フレームワークが Hono に統一されているか。他のフレームワークが混ざっていないか。
- OpenAPI 文書が実装から生成されているか。手書きの定義が別に残っていないか。
- パスが /api/v1/ 配下にあり、単数形で kebab-case か。
- すべての入力が c.req.valid() を通っているか。
- エラーが application/problem+json で、本文の status が HTTP ステータスと一致しているか。
- エラー本文に内部情報が漏れていないか。
- 公開 API に Bearer 認証があるか。loopback サービスに Origin と Host の検証があるか。
- 所有権の確認がハンドラかサービス層にあるか。
- /health が認証なしで到達でき、application/health+json を返すか。
- ミドルウェアの登録順が正しいか。onError と notFound が最後か。
- Spectral が CI で走っているか。
- HTTP 框架是否已统一为 Hono,且没有混入其他框架?
- OpenAPI 文档是否由实现生成,且没有残留单独手写的定义?
- 路径是否位于 /api/v1/ 下,并使用单数形式和 kebab-case?
- 所有输入是否都经过 c.req.valid()?
- 错误是否使用 application/problem+json,且正文中的 status 与 HTTP 状态一致?
- 错误正文是否未泄露内部信息?
- 公开 API 是否具有 Bearer 认证?loopback 服务是否验证 Origin 和 Host?
- 所有权检查是否位于 handler 或 service 层?
- /health 是否无需认证即可访问,并返回 application/health+json?
- 中间件注册顺序是否正确?onError 和 notFound 是否位于最后?
- Spectral 是否在 CI 中运行?