使用 Codex 重构后端接口时,经常会遇到这种需求:

原来的返回:

{
  "name": "Tom"
}

现在业务想改成:

{
  "firstName": "Tom",
  "lastName": "Lee"
}

从代码角度看,只是修改几个字段。

但如果这个 API 已经被多个前端、App、小程序或第三方系统使用,直接上线以后,很可能出现:

  • Web新版正常,旧版App突然白屏;

  • 某些客户端一直读取旧字段;

  • 一个字段从字符串改成对象后,大量解析代码报错;

  • 后端已经上线,新前端还没全部发布;

  • Codex修改DTO后,旧调用方全部编译或运行失败;

  • 为了修复兼容问题,又被迫紧急回滚接口。

真正的问题不是“字段不能改”,而是:

公开 API 一旦被使用,就已经形成了契约。


一、最危险的是直接删除旧字段

假设原接口:

{
  "userName": "Tom"
}

Codex 重构后直接改成:

{
  "displayName": "Tom"
}

新前端:

user.displayName

正常。

但旧客户端仍然:

user.userName

最终拿到:

undefined

如果后面继续:

user.userName.toUpperCase()

就可能直接报错。

因此 API 升级时一个非常重要的原则是:

先新增,再迁移,最后删除。


二、先让新旧字段共存

可以先返回:

{
  "userName": "Tom",
  "displayName": "Tom"
}

新客户端开始读取:

displayName

旧客户端继续读取:

userName

两边都能正常工作。

等确认旧客户端使用量已经接近0,再考虑删除:

userName

这和数据库迁移中的 Expand / Contract 思路很类似。

API 也应该允许一段时间的新旧结构共存。


三、字段改名比想象中风险更大

从后端角度:

name
→ displayName

看起来只是命名优化。

但调用方可能包括:

Web
iOS
Android
小程序
内部后台
第三方集成
自动化脚本

其中很多客户端并不能和服务端同时发布。

特别是 App。

用户可能几个月都没有升级。

所以:

服务端今天改字段

不代表:

所有客户端今天都会升级。

API 设计必须接受这种现实。


四、不要随意改变字段类型

例如原来:

{
  "price": "99.00"
}

为了“类型更正确”,Codex 改成:

{
  "price": 99
}

看起来合理。

但旧客户端可能写着:

price.split(".");

字段类型变化通常比字段新增更加危险。

类似的还有:

string → number
number → object
array → object
null → string

API字段一旦上线,类型本身也是契约的一部分。


五、增加字段通常比删除字段安全

例如原接口:

{
  "id": 1001,
  "name": "Tom"
}

新增:

{
  "id": 1001,
  "name": "Tom",
  "avatar": "..."
}

大多数客户端会直接忽略它不认识的字段。

因此通常:

新增可选字段

属于相对低风险修改。

而:

删除字段
改变类型
改变语义

属于高风险修改。

API Code Review 时应该区分这两类变化。


六、请求参数也要保持兼容

不只是响应。

例如原接口:

GET /orders?status=paid

新版本想改成:

GET /orders?state=paid

如果直接删除:

status

旧客户端就会失效。

可以先支持:

status
和
state

一段时间。

例如:

const state =
  req.query.state ??
  req.query.status;

并在日志中统计:

还有多少请求继续使用旧参数?

等旧参数流量足够低,再真正删除。


七、用Deprecated明确标记旧字段

不要让旧字段永久存在。

可以在代码或API文档中标记:

interface UserResponse {
  /**
   * @deprecated Use displayName instead.
   */
  userName: string;

  displayName: string;
}

这样开发者使用旧字段时,IDE 会提示:

Deprecated

同时文档明确说明:

替代字段
计划移除时间
影响版本

兼容并不是永远不删除。

而是:

给调用方一个可预期的迁移窗口。


八、什么时候需要API Versioning?

如果变化已经无法保持向后兼容,例如:

请求结构彻底改变
返回模型完全重构
业务语义发生变化

可以考虑 API 版本。

常见形式:

/api/v1/users
/api/v2/users

也可以通过 Header:

Accept-Version: 2

具体方式取决于系统设计。

例如:

v1
继续返回旧结构

v2
返回新结构

这样旧客户端不会因为服务端升级立即失效。


九、不要为了一个小字段就无限增加版本

API Versioning 也有成本。

如果:

改一个字段
→ v2

加一个字段
→ v3

调整排序
→ v4

版本很快就会失控。

版本更适合:

无法兼容的重大契约变化

普通新增字段通常不需要新版本。

可以简单理解:

兼容修改
→ 当前版本继续演进

破坏性修改
→ 考虑新版本

十、多个版本不能永久同时维护

假设系统长期存在:

v1
v2
v3
v4

每修一个Bug都要同步修改四套实现。

维护成本会越来越高。

因此新版本上线后,要定义旧版本生命周期:

v1 Deprecated
↓
停止新增功能
↓
通知调用方迁移
↓
观察调用量
↓
停止服务

API Versioning 的目标不是永久保存所有历史代码。


十一、用适配层减少重复业务逻辑

不要写:

v1 controller
→ 一套业务代码

v2 controller
→ 再复制一套业务代码

更合理的是:

v1请求
↓
转换成内部模型
↓
统一Service

v2请求
↓
转换成内部模型
↓
统一Service

返回时:

内部结果
↓
v1 serializer

内部结果
↓
v2 serializer

也就是说:

版本差异尽量放在协议边界,核心业务逻辑保持一套。


十二、不要让数据库模型直接变成API模型

例如:

return db.user.findUnique(...);

数据库字段一改:

数据库Schema变化
↓
API响应也跟着变化

风险很高。

更推荐使用明确 DTO:

function toUserResponse(user: UserEntity) {
  return {
    id: user.id,
    displayName: user.name
  };
}

这样数据库内部怎么改,不会自动影响外部 API 契约。


十三、DTO可以成为兼容边界

例如数据库已经拆成:

first_name
last_name

但旧API仍然需要:

name

可以在 DTO 层组合:

return {
  name:
    `${user.firstName} ${user.lastName}`,
  firstName: user.firstName,
  lastName: user.lastName
};

数据库可以完成内部升级。

API则按照自己的节奏逐步迁移。

这能明显降低“数据库改一下,所有客户端一起跟着改”的耦合。


十四、枚举值也属于API契约

假设原来:

{
  "status": "paid"
}

现在增加:

partially_refunded

旧客户端可能只处理:

switch (status) {
  case "paid":
  case "cancelled":
}

遇到新状态以后可能出现未知行为。

因此增加新枚举值也应该评估兼容性。

客户端最好提供:

unknown / default

兜底分支。

服务端也不能假设“只是新增一个字符串,不会影响旧客户端”。


十五、null和字段缺失不是一回事

原接口:

{
  "avatar": null
}

如果改成:

{}

某些客户端行为可能不同。

例如:

avatar = null

可能表示:

明确没有头像

而字段完全不存在可能表示:

接口版本不支持
或数据尚未加载

API应尽量保持稳定语义。

不要为了减少几个字节随意改变 null、空字符串和字段缺失之间的约定。


十六、分页格式也不要随意重构

例如旧API:

{
  "list": [],
  "page": 1,
  "total": 100
}

Codex为了统一格式改成:

{
  "data": [],
  "meta": {
    "page": 1,
    "total": 100
  }
}

这属于明显的破坏性变化。

如果已有大量调用方,最好:

创建v2

或者提供足够长的迁移期。

“结构更漂亮”不是直接破坏兼容性的理由。


十七、错误响应同样需要版本稳定

例如旧API:

{
  "error": "USER_NOT_FOUND"
}

新代码改成:

{
  "code": 40401,
  "message": "User not found"
}

如果前端依赖:

if (
  response.error ===
  "USER_NOT_FOUND"
)

所有错误处理逻辑都会失效。

因此错误码应该比错误文案更加稳定。

推荐使用:

稳定code
+
可变化message

例如:

{
  "code": "USER_NOT_FOUND",
  "message": "User does not exist"
}

十八、Contract Test可以提前发现兼容问题

如果只测试服务端:

接口返回200

并不能说明旧客户端还能用。

可以加入 Contract Test。

例如明确验证:

/user接口必须仍然包含:
id
userName
displayName

或者通过 OpenAPI Schema 检查:

是否删除已有字段?
是否改变字段类型?
是否增加必填参数?

CI发现破坏性修改后直接提示。

这样比上线以后由旧客户端报错安全得多。


十九、OpenAPI变更可以自动做Breaking Change检查

如果项目维护:

openapi.yaml

就可以比较:

旧版本Schema
vs
新版本Schema

重点检查:

删除字段
修改字段类型
新增required请求参数
修改响应状态码
删除endpoint

这些通常属于 Breaking Change。

Codex生成API代码后,也可以要求:

同时检查OpenAPI是否产生破坏性变化。

二十、一定要统计旧版本使用量

准备删除:

userName

之前,不应该只问:

前端团队说改完了吗?

还应该通过日志或 Metrics 查看:

仍然有多少请求来自旧App?
多少客户端继续访问v1?
旧参数status还有没有被使用?

例如:

v1流量占比:
12%

显然还不能直接关闭。

如果已经:

0.02%

再结合业务情况决定退役时间。


二十一、移动端特别需要保守兼容

Web应用通常可以快速发布。

但 App 用户可能长期停留在旧版本。

例如:

App 8.1
App 8.2
App 9.0

三种版本同时在线。

如果后端只兼容最新客户端,旧App很容易突然失效。

因此移动端 API 通常需要更长兼容窗口。

如果旧版本必须停止支持,也应该有:

最低版本检查
升级提示
明确退役时间

而不是让接口随机报错。


二十二、让Codex先做Breaking Change审查

修改API前可以这样输入:

请先不要修改代码。

分析这次API调整:

1. 哪些字段会新增;
2. 哪些字段会删除;
3. 哪些字段类型会变化;
4. 是否新增required参数;
5. 是否改变错误码;
6. 旧客户端是否还能继续使用;
7. 是否可以通过新旧字段共存解决;
8. 是否真的需要新增API版本。

先判断是不是 Breaking Change,再开始写代码。


二十三、测试必须保留旧客户端场景

例如新接口上线后同时测试:

新客户端
→ 使用displayName
→ 正常

以及:

旧客户端
→ 继续使用userName
→ 仍然正常

还要测试:

旧请求参数
旧错误处理
旧分页格式
未知枚举值

不要只验证新版本功能。


二十四、把API兼容规则写进AGENTS.md

# API兼容规则

- 已发布API默认视为外部契约
- 禁止直接删除正在使用的响应字段
- 字段改名必须优先使用新旧字段共存
- 禁止无版本升级直接改变字段类型
- 新增required请求参数必须评估旧客户端
- 数据库Entity禁止直接作为公开API响应
- 破坏性修改必须评估API Versioning
- Deprecated字段必须注明替代方案
- 删除旧版本前必须检查真实调用量
- 修改API后必须执行Breaking Change检查

这样 Codex 后续重构接口时,就不会只追求:

代码结构更漂亮。

还会同时考虑:

线上已经存在的调用方。

二十五、Plus还是Pro?

如果主要使用 Codex 处理:

  • 单接口DTO;

  • 普通字段调整;

  • 小型前后端项目;

  • 简单OpenAPI;

Plus通常已经能够覆盖大部分开发任务。

如果长期维护:

  • 大型公共API;

  • 多端客户端;

  • 多API版本;

  • 大量OpenAPI Schema;

  • 第三方集成;

  • 多仓库同步改造;

则可以根据实际开发强度评估 Pro。

Pro更适合需要持续检查大量调用方、接口定义和多文件兼容逻辑的场景。

不过无论使用哪个版本,API升级都应该遵循同一个原则:

服务端可以快速发布,但调用方不一定能同时升级。

总结

Codex 修改 API 字段以后,为什么新版本正常,旧客户端却突然全部报错?

因为 API 不只是后端代码。

它本质上是:

服务端
和
所有调用方

共同遵守的一份契约。

通过:

向后兼容
新旧字段共存
Deprecated
API Versioning
DTO隔离
Contract Test

可以让接口从一次性“硬切换”,变成安全的渐进升级。

真正成熟的 API 重构,不是让最新版客户端能跑。

而是做到:

新客户端可以逐步使用新结构,旧客户端仍然有迁移时间,直到所有调用方真正完成升级后,再安全删除旧协议。

CSDN文章描述

本文介绍 Codex 修改 API 字段时常见的旧客户端兼容问题,并通过向后兼容、Deprecated、API Versioning、DTO隔离和 Contract Test,降低接口升级带来的 Breaking Change 风险。

Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。

更多推荐