Appearance
RESTful API 设计规范与移动端契约约定
回到总览:后台 / API / 数据层
相关模块:API 幂等性、超时重试与移动端数据最终一致性 · Token 鉴权体系、JWT、OAuth2 与移动端双 Token 自动无感刷新
一句话定义
RESTful API 设计规范是指基于 HTTP 协议语义,利用 URI 表达资源(Resource)、HTTP 谓词(GET/POST/PUT/DELETE)表达操作动作、标准 HTTP 状态码与统一 JSON 结构体(Status/Code/Message/Data)定义移动端与后端之间高效、版本透明且高可用的通信契约。
代码索引
计划补齐的实验:
labs/shared/backend-data/rest-api-demo/— RESTful 资源映射、统一 Response 包装与 API 异常状态码契约测试
为什么 requires
- 为什么在移动端 API 设计中,后端统一返回
200 OK并在 JSON 内部自定义code: 500的做法属于防守性反模式?- 一句话答:统一返回 200 会掩盖 HTTP 网络层的真实语义,导致 OkHttp/Retrofit/Dio 拦截器、CDN 缓存代理、APM 监控工具无法通过标准 HTTP 状态码识别错误,破坏了基础设施的通用监控能力。
- 为什么 10 年 Android 工程师做 API 契约评审时要强调“显式版本号控制”?
- 一句话答:移动端应用(APP)发布后无法强制用户立刻升级;当后端重构或修改字段类型时,必须通过 URI 版本号(如
/api/v1/)隔离新旧契约,保障旧版本 App 用户不受破坏性变更(Breaking Change)影响。
- 一句话答:移动端应用(APP)发布后无法强制用户立刻升级;当后端重构或修改字段类型时,必须通过 URI 版本号(如
底层机制
1. 标准 RESTful 资源与动作映射
text
请求路径 (URI 资源描述) HTTP 动词 语义说明
/api/v1/users GET 获取用户列表
/api/v1/users/10086 GET 获取 ID 为 10086 的用户详情
/api/v1/users POST 创建新用户
/api/v1/users/10086 PUT 完整更新 ID 为 10086 的用户
/api/v1/users/10086 DELETE 删除 ID 为 10086 的用户2. 统一 JSON 响应体 (Response Wrapping) 结构
json
{
"code": 0,
"message": "success",
"data": {
"userId": "10086",
"nickname": "Alex",
"avatar": "https://cdn.example.com/avatar.jpg"
},
"timestamp": 1688001234000
}常见状态码契约标准
| HTTP 状态码 | 业务含义 | 移动端处理建议 |
|---|---|---|
200 OK | 请求成功 | 解析 data 节点展示 UI |
400 Bad Request | 客户端参数校验错误 | 读取 message 进行 Toast 提示 |
401 Unauthorized | 未登录或 Token 已过期 | 触发静默刷新 Token 或跳转登录页 |
403 Forbidden | 无权限访问该资源 | 弹出无权限提示 |
404 Not Found | 资源不存在 | 展现空状态页面 |
500 Internal Error | 服务端崩溃/内部异常 | 展示通用错误提示,触发 APM 告警 |
Android / Flutter / Web / Backend 对照
| 维度 | Android (Retrofit) | Flutter (Dio) | Web (Axios) |
|---|---|---|---|
| 网络框架 | Retrofit + Gson/Moshi | Dio + json_serializable | Axios |
| 契约代码生成 | OpenAPI / Swagger / Protobuf | build_runner 序列化生成 | TypeScript Interface 自动生成 |
| 错误拦截 | Retrofit ErrorBody 解析 | DioException 拦截 | Axios Response Interceptor |
常见场景
1. Retrofit 声明 RESTful 契约与统一包装
java
public interface UserApiService {
@GET("api/v1/users/{id}")
Call<ApiResponse<User>> getUserById(@Path("id") String userId);
@POST("api/v1/users")
Call<ApiResponse<User>> createUser(@Body UserCreateRequest request);
@PUT("api/v1/users/{id}")
Call<ApiResponse<User>> updateUser(@Path("id") String userId, @Body UserUpdateRequest request);
@DELETE("api/v1/users/{id}")
Call<ApiResponse<Void>> deleteUser(@Path("id") String userId);
}常见误配、事故后果与排障
1. 事故:后端将可空字段直接返回 null 或丢失字段导致移动端解析崩溃
- 误配原因:后端将整型字段返回了
""(空字符串),或将 List 返回了null。 - 后果:Android 的 Gson/Moshi 或 Flutter 的
json_serializable在强类型反序列化时抛出JsonDataException,导致 App 页面直接崩溃。 - 排障与修法:前后端统一契约约束:列表无数据必须返回空数组
[],不得返回null;字符串无数据可返回空串或不设必填。
与相近概念对比
| 架构风格 | 交互模式 | 优点 | 适合场景 |
|---|---|---|---|
| RESTful API | HTTP 动词 + URI 资源 | 标准通用,生态极其成熟,支持 HTTP 缓存 | 绝大多数移动 App / Web 业务 |
| GraphQL | 单一 EndPoint,客户端按需指定 Schema | 按需返回字段,消除 Over-fetching | 复杂数据嵌套、多终端自定义字段 |
| gRPC (Protobuf) | HTTP/2 二进制序列化 | 性能极高,强类型契约,体积微小 | 微服务内部通信、实时高频 App 交互 |
对应实验
计划补齐的实验:
labs/shared/backend-data/rest-api-demo/— RESTful 资源映射、统一 Response 包装与 API 异常状态码契约测试
复习检查题
在 RESTful 架构中,HTTP
PUT和PATCH动词有何区别?答:
PUT动词用于资源的完整替换更新,客户端必须提交目标资源的完整最新数据结构;而PATCH动词用于资源的局部增量更新,客户端仅需要提交被修改的那些字段。为什么移动端 API 强烈不建议将数据列表直接反序列化为顶层 JSON 数组(
[ {...}, {...} ]),而是推荐用对象包裹({ "list": [...] })?答:因为顶层 JSON 数组扩展性极差。如果后续需要增加分页信息(如
hasMore、nextCursor)或汇总统计字段,顶层数组格式将无法在不破坏现有反序列化结构的情况下添加这些元数据;而用对象包裹的形式则可以非常平滑地扩展添加任何新的 Header/Meta 字段。
速记
- 动词资源化:URI 描述资源,GET 查、POST 创、PUT 换、DELETE 删。
- 错误码分层:HTTP 状态码表达网络/通用层,内部 code 表达细化业务层。
- 契约防护:列表无数据返空数组
[]禁null,版本号隔离 Breaking Change。