Hono 工程化实战 08:接口版本、灰度发布和迁移策略
wxk1991 Lv5

Hono 工程化实战 08:接口版本、灰度发布和迁移策略

API 最难的不是写出来。

最难的是改。

一个接口上线后,可能已经被这些地方依赖:

1
2
3
4
5
6
7
Web 前端
移动端
后台管理
第三方客户
自动化脚本
定时任务
测试用例

你以为只是改了一个字段。

别人那边可能直接白屏。

Hono 很适合快速开发 API,但越是快,越要认真设计接口版本、发布和迁移策略。

这篇文章讲 Hono 项目如何安全地演进 API:版本号、兼容变更、breaking change、灰度发布、数据库迁移、回滚。

技术栈快照时间:2026-06-23。Hono 路由、OpenAPI、RPC、部署平台能力会更新,生产使用前以官方文档为准。


一、先定义什么是 breaking change

不是所有改动都需要升版本。

通常不算 breaking change:

1
2
3
4
5
新增可选字段
新增接口
新增错误详情字段
提高非核心限流额度
响应里新增 meta

通常算 breaking change:

1
2
3
4
5
6
7
8
9
删除字段
字段改名
字段类型变化
字段语义变化
状态码变化
错误码变化
分页规则变化
鉴权方式变化
默认排序变化

比如:

1
2
3
4
{
"id": "post_123",
"title": "Hello"
}

改成:

1
2
3
4
{
"postId": "post_123",
"title": "Hello"
}

这就是 breaking change。

即使你觉得 postId 更清晰,也不能直接改。


二、路径版本最适合大多数项目

常见 API 版本方式:

1
2
3
4
/api/v1/posts
/api/v2/posts
Accept: application/vnd.example.v1+json
Header: X-API-Version: 1

对大多数 Hono 项目,路径版本最直观。

1
2
3
4
5
6
7
8
9
10
const app = new Hono()

const v1 = new Hono()
const v2 = new Hono()

v1.get('/posts/:id', getPostV1)
v2.get('/posts/:id', getPostV2)

app.route('/api/v1', v1)
app.route('/api/v2', v2)

优点:

1
2
3
4
浏览器和 curl 都直观
文档清晰
网关和日志容易区分
前端迁移容易分阶段

缺点:

1
2
3
路径变长
版本 route 可能重复
需要维护多个版本

但这个成本通常值得。


三、不要每次改动都 v2

版本不是越多越专业。

如果只是新增字段:

1
2
3
4
5
{
"id": "post_123",
"title": "Hello",
"summary": "新增字段"
}

旧前端通常不会坏。

这不需要 v2。

如果要重命名字段,可以先兼容:

1
2
3
4
5
{
"id": "post_123",
"postId": "post_123",
"title": "Hello"
}

然后标记 id 将废弃。

迁移完成后,再在下一个大版本删除。

API 演进优先考虑兼容。

版本升级是最后手段,不是日常习惯。


四、加 Deprecation Header

如果某个接口要废弃,可以在响应头里提示:

1
2
3
4
5
6
7
app.get('/api/v1/posts/:id', async (c) => {
c.header('Deprecation', 'true')
c.header('Sunset', 'Wed, 31 Dec 2026 23:59:59 GMT')
c.header('Link', '<https://docs.example.com/migrate-v2>; rel="deprecation"')

return getPostV1(c)
})

这样客户端和监控系统都能看到:

1
这个接口还可用,但已经进入废弃周期

不要今天说废弃,明天就删。

给迁移窗口。

尤其是第三方 API。


五、灰度发布不等于多版本 API

API 版本解决的是契约变化。

灰度发布解决的是新实现逐步放量。

比如同一个 /api/v1/search,你想从旧搜索切到新搜索:

1
2
3
4
5
6
7
8
9
app.get('/api/v1/search', async (c) => {
const user = c.get('user')

if (await shouldUseNewSearch(user.id, c.env)) {
return newSearch(c)
}

return oldSearch(c)
})

灰度条件可以是:

1
2
3
4
5
6
指定用户
指定租户
百分比流量
内部员工
请求 header
环境变量

灰度的关键不是开关。

关键是可回滚。

新实现有问题时,要能快速切回旧实现。


六、数据库迁移要兼容代码发布

很多线上事故来自数据库迁移和代码发布不同步。

安全迁移通常分三步。

第一步:加字段,不删旧字段。

1
alter table posts add column summary text;

代码同时兼容新旧字段。

第二步:回填数据。

1
2
3
update posts
set summary = substr(content, 1, 120)
where summary is null;

第三步:代码完全切到新字段后,再考虑删除旧字段。

危险做法是:

1
2
先删字段
再发代码

如果代码还在读旧字段,接口会直接炸。

数据库迁移要按“可回滚、可兼容、可重复执行”来设计。


七、响应结构要有稳定外壳

我建议 API 响应保持稳定外壳:

成功:

1
2
3
4
{
"data": {},
"meta": {}
}

失败:

1
2
3
4
5
6
7
{
"error": {
"code": "POST_NOT_FOUND",
"message": "文章不存在"
},
"requestId": "01JZ..."
}

这样以后新增分页、追踪信息、实验标记,都可以放进 meta

1
2
3
4
5
6
7
{
"data": [],
"meta": {
"nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA2LTIzIn0",
"experiment": "search_v2"
}
}

稳定外壳能减少 breaking change。

不要今天返回数组,明天返回对象,后天再包一层 data。


八、Hono route 如何组织版本

目录可以这样:

1
2
3
4
5
6
7
8
src/
routes/
v1/
posts.ts
users.ts
v2/
posts.ts
users.ts

组装:

1
2
3
4
5
6
7
8
import { Hono } from 'hono'
import { v1Routes } from './routes/v1'
import { v2Routes } from './routes/v2'

const app = new Hono()

app.route('/api/v1', v1Routes())
app.route('/api/v2', v2Routes())

但不要复制所有代码。

业务 service 可以共享:

1
2
3
v1 route -> v1 response mapper
v2 route -> v2 response mapper
shared service -> shared business logic

版本差异尽量放在接口层。

不要为了 v2 复制一整套业务逻辑。


九、发布前做契约检查

每次改 API 前,问自己:

1
2
3
4
5
6
7
请求参数有没有变
响应字段有没有删
字段类型有没有变
错误码有没有变
状态码有没有变
分页规则有没有变
鉴权要求有没有变

如果有 breaking change:

1
2
3
4
5
6
能否兼容一段时间
是否需要新版本
是否写了迁移说明
是否通知调用方
是否保留旧接口
是否有下线时间

这套检查可以写进 PR 模板。

API 变更不能只靠开发者临时想起。


十、回滚策略

发布前要知道怎么回滚。

回滚分几类:

1
2
3
4
5
代码回滚
配置回滚
功能开关回滚
数据库迁移回滚
流量切换回滚

最容易的是功能开关:

1
NEW_SEARCH_ENABLED=false

最难的是数据库回滚。

所以数据库变更要尽量向前兼容。

比如:

1
2
3
4
5
6
先加字段
代码双写
回填数据
代码读新字段
观察稳定
最后删旧字段

这样中间任何一步出问题,都有退路。


十一、版本下线要看真实流量

不要只凭感觉下线旧版本。

先看日志:

1
2
3
4
/api/v1/posts 是否还有请求
调用方是谁
错误率是否升高
User-Agent 或 API key 是什么

如果还有客户在用,就通知。

如果是内部前端在用,就安排迁移。

如果是未知调用方,就先加告警和 deprecation header。

下线 API 是产品行为,不只是技术行为。


十二、推荐落地流程

一个成熟一点的 Hono API,可以这样管理发布:

1
2
3
4
5
6
7
8
9
1. 默认保持向后兼容
2. breaking change 才引入新版本
3. 废弃接口加 Deprecation/Sunset header
4. 新实现用功能开关灰度
5. 数据库迁移分阶段做
6. 响应结构保持 data/error/meta 外壳稳定
7. 发布前跑测试和 smoke test
8. 发布后看错误率、耗时和旧版本流量
9. 下线前通知调用方并确认真实流量

Hono 让路由组织很轻。

但 API 的生命周期不轻。

接口不是写完就结束。

真正的工程化,是让接口能被安全地修改、灰度、回滚和下线。