如何设计一个接口
1. 核心目标
接口(API)的本质是一份可演进的“契约(Contract)”:调用方基于契约构造请求,服务方基于契约返回可解释的结果,并在长期迭代中保持兼容或给出清晰迁移路径。
设计接口时要优先保证 3 个结果:语义正确、行为稳定、可观测可治理。这 3 点不闭环,接口通常会在幂等、兼容、错误处理、性能或安全上出现问题。
1.1 接口
- HTTP REST API(最常见)。
- RPC(如 gRPC、Dubbo)。
- 消息接口(Kafka topic、MQ queue)。
本笔记以 HTTP API 为主,同时在关键点给出 REST 与 RPC 的对比取舍。
1.2 设计维度总览
一个可上线可长期维护的接口,至少要回答下面这些问题:
- 业务:这个接口解决什么问题,成功与失败各代表什么业务状态。
- 协议:路径、方法、状态码、请求头、幂等、缓存语义。
- 契约:字段 schema、校验规则、错误模型、分页排序、兼容策略。
- 治理:鉴权、限流、超时重试、灰度、监控、审计、版本下线。
2. 需求澄清与边界定义
2.1 调用方与使用场景
先问清楚谁在调用、怎么调用、失败怎么补偿:
- 调用方:Web 前端、App、内部服务、第三方合作方。
- 调用链:是否跨机房、是否经网关、是否会自动重试、是否经队列异步。
- 场景:读多写少、强一致写入、批量导入、实时链路(低延迟)等。
这些信息会直接决定认证方式、限流策略、超时、幂等、分页方式和返回结构。
2.2 SLA 与失败形态
把“可用性目标”明确成可验证的指标:
- 延迟:P50、P90、P99 上限。
- 错误率:4xx/5xx 占比,业务错误码分布。
- 限制:QPS 峰值、突发流量、请求体大小上限。
同时要明确失败形态:同步失败(立即返回错误)、异步失败(任务失败回查)、部分成功(批量接口)。
2.3 数据分级与合规边界
接口涉及的字段要做数据分级与合规边界(尤其是对外):
- 身份类数据:手机号、证件号、邮箱,需要脱敏与审计。
- 业务敏感:订单、余额、权限信息,需要最小授权与风控。
- 个人隐私与合规:数据留存、删除、导出能力可能有强约束。
3. 资源建模与 HTTP 语义
3.1 资源建模:用名词表达业务实体
REST 风格下,路径表达资源,动词交给方法:
- 推荐:
/users/{userId}/orders/{orderId} - 避免:
/createOrder、/queryOrder(把方法语义塞进路径,扩展后会混乱)
当业务不是天然资源(如“计算报价”“风控校验”),可以用“动作资源”承载:/quotes、/risk-assessments,把一次计算视作可追踪的资源实例。
3.2 方法语义:安全性与幂等性要区分
HTTP 方法语义决定了缓存、重试、网关治理策略。面试要能清晰区分:
- 安全(safe):不会改变服务端状态(典型
GET)。 - 幂等(idempotent):同一请求执行一次或多次,最终效果一致(典型
PUT、DELETE)。
| 方法 | 常见用途 | 是否安全 | 是否幂等 | 典型返回 |
|---|---|---|---|---|
GET |
查询资源 | 是 | 是 | 200,无资源可 404 |
POST |
创建或触发动作 | 否 | 否(可通过幂等键实现“业务幂等”) | 创建可 201,异步可 202 |
PUT |
全量替换资源 | 否 | 是 | 200 或 204 |
PATCH |
部分更新资源 | 否 | 语义上不一定 | 200 或 204 |
DELETE |
删除资源 | 否 | 是 | 204(重复删除仍可 204) |
3.3 状态码与错误语义:不要“永远 200”
状态码承载的是协议语义,业务错误应配合结构化错误体表达。常见映射如下:
| 场景 | 推荐状态码 | 说明 |
|---|---|---|
| 参数校验失败 | 400 |
请求不合法,通常不该重试 |
| 未认证 | 401 |
缺 token 或 token 无效 |
| 无权限 | 403 |
已认证但无权限 |
| 资源不存在 | 404 |
对外接口注意避免枚举攻击泄露存在性 |
| 并发写冲突 | 409 |
乐观锁冲突,提示重试或刷新 |
| 频率限制 | 429 |
配合 Retry-After |
| 服务端异常 | 500 |
服务 bug 或未捕获异常 |
| 上游超时或不可用 | 502、503、504 |
网关/代理更常见 |
3.4 REST 与 RPC 的对比取舍
选择接口风格时,面试不需要“站队”,要能讲清楚约束与治理成本:
| 维度 | REST(HTTP + JSON) | RPC(gRPC/自研协议) |
|---|---|---|
| 语义表达 | 借助资源与方法语义,天然适合 CRUD | 更像函数调用,表达灵活 |
| 治理与网关 | 生态成熟(鉴权、限流、缓存、观测) | 需要更强的平台能力配套 |
| 性能 | 文本协议开销更大 | 二进制更省,适合高吞吐 |
| 兼容演进 | JSON 兼容依赖客户端容错 | Protobuf 需遵守 tag 演进规则 |
| 对外开放 | 更容易被第三方理解与调试 | 一般更偏内部服务调用 |
4. 请求与响应契约(Contract)
4.1 字段设计:命名、类型、默认值
字段契约要做到“看一眼就不误用”:
- 命名:统一风格(如
snake_case或camelCase),不要一会userId一会user_id。 - 类型:金额用
string或整数分(避免浮点误差),时间统一RFC 3339(如2026-03-28T17:30:00+08:00)。 - 默认值:明确“缺省字段”与“显式空值”的差异,避免
null语义混乱。
4.2 输入校验:把不变量尽早失败
校验规则要可落地到代码与网关:
- 结构校验:必填、长度、枚举、正则。
- 语义校验:跨字段约束(如
start_time <= end_time)。 - 权限校验:避免先查全量数据再过滤,优先“授权即查询范围”。
4.3 响应结构:统一 envelope,便于治理
对内接口可以更自由;对外或跨团队接口建议统一 envelope,至少包含:
code:稳定的业务错误码(用于告警聚合与客户端分支)。message:面向开发者的简短说明。request_id:串联日志与链路追踪。data:成功时的业务数据。
成功示例(最小可复现):
{
"code": "OK",
"message": "success",
"request_id": "01J9Z8Z1JQ8V2P6E1Q9J4W8Z3K",
"data": {
"order_id": "o_123",
"status": "PAID"
}
}
失败示例:
{
"code": "ORDER_NOT_FOUND",
"message": "order not found",
"request_id": "01J9Z8Z1JQ8V2P6E1Q9J4W8Z3K",
"data": null
}
4.4 分页、排序与过滤:避免“全量列表接口”
分页有两类主流方案:
| 方案 | 请求参数 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| Offset 分页 | page + page_size |
简单直观 | 数据变动会漂移,深分页慢 | 管理后台、小数据量 |
| Cursor 分页 | cursor + limit |
稳定,适合大数据 | 客户端实现稍复杂 | Feed、订单流水 |
Cursor 分页响应示例:
{
"code": "OK",
"message": "success",
"request_id": "01J9Z8...",
"data": {
"items": [],
"next_cursor": "c_456",
"has_more": false
}
}
4.5 错误码设计:让客户端能稳定分支
错误码要做到“稳定、可检索、可聚合”:
- 稳定:错误码一旦对外发布,不能随意改名或改语义。
- 可检索:最好能映射到文档与排查建议。
- 可聚合:同一类问题不要造很多码,避免监控碎片化。
建议把错误码按领域划分前缀,例如 ORDER_、PAYMENT_、AUTH_,并为每个错误码给出“是否可重试”的建议。
5. 幂等、重试与并发控制
5.1 为什么必须考虑幂等
只要链路中存在以下任一情况,就必须设计幂等策略:
- 客户端超时后重试(用户多点一次、SDK 自动重试)。
- 网关或负载均衡重试。
- 消息系统至少一次投递(at-least-once)。
否则就会出现“重复扣款、重复下单、重复发货”等典型事故。
5.2 幂等实现:接口幂等与业务幂等
常见做法是对 POST 引入幂等键(Idempotency-Key),服务端做去重:
- Key 维度:
(caller_id, idempotency_key, api_name)。 - 存储:Redis(带 TTL)或数据库唯一索引(强一致)。
- 返回:相同 key 返回第一次的结果(包括相同状态码与响应体)。
需要明确两类语义:
- 接口幂等:同一请求重复提交,返回同一结果。
- 业务幂等:同一业务意图(如同一支付单号)只生效一次,即使请求体略有差异也不允许重复。
5.3 并发写:乐观锁与条件请求
并发更新避免“最后写入覆盖”:
- 数据层乐观锁:增加
version字段,UPDATE ... WHERE id = ? AND version = ?,成功后version = version + 1。 - HTTP 条件请求:返回
ETag,更新时带If-Match,不匹配返回412 Precondition Failed或用409表达冲突。
关键点是让客户端能判断“是否需要刷新再提交”,而不是盲目重试。
5.4 超时与重试:把重试做成可控策略
面试里要强调“重试不是万能药”,要配合:
- 超时:区分连接超时、读超时,设置合理上限(避免线程被占满)。
- 退避:指数退避 + 抖动(jitter),避免雪崩重试。
- 预算:全链路超时预算(timeout budget),下游超时必须小于上游。
- 可重试性:只有幂等或可证明安全的请求才允许自动重试。
5.5 落库与事件:写接口要考虑一致性边界
当接口写入后需要发消息、写缓存或触发异步流程时,要明确一致性模型:
- 同库事务内:业务表与幂等表、版本字段一起提交。
- 跨系统:优先用 outbox(本地事务写出站表,再异步投递),避免“写库成功但发消息失败”。
面试里讲清楚“我能定义边界并选一个可验证的落地方案”,比背概念更重要。
6. 安全、权限与风控
6.1 认证与鉴权:别把 JWT 当万能
常见组合:
- 对外:OAuth 2.0 access token + scope(或 API key + 签名)。
- 对内:mTLS 或网关签发身份(配合零信任)。
注意点:
- JWT 无状态不等于“不可控”,需要配合
exp、jti、黑名单或 token 轮换策略。 - 鉴权要做“最小授权”,把权限与资源范围绑定(如只能访问本租户数据)。
6.2 输入安全与防护
至少要覆盖:
- 参数白名单与类型校验,避免注入与越权。
- 频控与限流:按用户、IP、应用维度;返回
429并给出重试提示。 - 防重放:签名 + 时间戳 + nonce(尤其是对外开放平台)。
6.3 Web 场景补充:CORS 与 CSRF
如果接口会被浏览器直接调用,需要额外考虑:
- CORS:只允许可信 Origin,避免
Access-Control-Allow-Origin: *搭配敏感接口。 - CSRF:基于 Cookie 的认证要配合 SameSite 或 CSRF token,基于 Bearer token 的接口相对更友好。
7. 性能、可用性与可扩展性
7.1 缓存与条件 GET
查询接口要考虑缓存策略:
- 服务端缓存:热点资源缓存、缓存击穿与穿透治理。
- HTTP 缓存:
Cache-Control、ETag,命中后返回304 Not Modified。
避免“所有查询都打 DB”,否则扩容成本会线性上升。
7.2 同步与异步:长耗时接口不要硬扛
长耗时操作(导出、批处理、复杂计算)建议改为异步模型:
POST /exports返回202+task_id。GET /exports/{task_id}查询状态。- 完成后回调 webhook 或提供下载链接。
这样可以避免线程长期占用,提升系统吞吐与稳定性。
7.3 批量接口:明确“部分成功”的语义
批量写入常见陷阱是“部分成功”。需要明确策略:
- 全有或全无:事务化,失败整体回滚,响应更简单。
- 部分成功:每个 item 返回独立结果与错误码,客户端可重试失败项。
部分成功建议返回结构化结果,避免靠字符串解析。
7.4 降级与保护:让接口在高峰期可控
先保护系统,再保护功能。
- 资源隔离:线程池隔离、连接池隔离,避免慢接口拖垮快接口。
- 限流:令牌桶或漏桶,按租户或用户维度治理。
- 熔断:下游异常时快速失败,避免级联超时。
8. 可观测性与可运维
8.1 关联标识:请求级与业务级都要有
至少要能做到:
- 请求级:
request_id(网关生成或透传)。 - 链路级:W3C Trace Context(如
traceparent)用于分布式追踪。 - 业务级:
order_id、payment_id等关键字段进日志(注意脱敏)。
8.2 指标与告警:接口维度的 SLI
接口发布前就要定义 SLI:
- 延迟:P50、P90、P99。
- 错误率:按
code、按 5xx、按超时。 - 饱和度:线程池、连接池、队列堆积。
没有这些指标,接口出了问题只能“翻日志猜”,定位效率会很差。
8.3 审计与回放:对外接口尤其重要
对外接口建议具备:
- 关键操作审计日志(谁在什么时候对什么资源做了什么)。
- 请求与响应脱敏留档(用于争议处理与合规审计)。
9. 版本管理与兼容性
9.1 版本策略:优先向后兼容
版本化常见方式:
| 方式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URI 版本 | /v1/orders |
直观 | 容易滥用,导致版本爆炸 |
| Header 版本 | Accept: application/vnd.xx.v1+json |
更规范 | 调试成本略高 |
| 参数版本 | ?version=1 |
简单 | 容易被忽略,治理较差 |
实践上更重要的是兼容原则:
- 向后兼容:新增字段尽量可选;枚举新增值要考虑旧客户端。
- 破坏性变更:必须给出迁移期与下线时间,并提供灰度或双写策略。
9.2 Schema 演进:让旧客户端不崩
兼容的本质是客户端解析不失败:
- JSON:客户端通常能忽略未知字段,但要避免改变字段类型与语义。
- Protobuf:字段号(tag)不可复用,删除字段要保留 tag,避免语义复活。
9.3 契约治理:用工具把“口头约定”变成“可验证”
建议做到:
- 用 OpenAPI 或 IDL 固化契约,并在 CI 做契约校验。
- 用契约测试(contract test)避免服务端改动导致客户端解析崩溃。
- 提供 deprecate 策略:公告、监控调用量、双版本并行、定期下线。
11. 面试精简回答
设计一个接口我会先把它当成“可演进的契约”来做:先澄清调用方、SLA、失败形态与是否会重试,再做资源建模与 HTTP 语义选择(方法、状态码、缓存语义)。然后把请求响应的 schema、校验规则、错误码体系和分页策略定清楚,保证客户端不误用。写接口一定要考虑幂等与并发控制(幂等键、乐观锁、条件请求),同时把超时、重试、限流做成可控策略。最后补齐安全(认证鉴权、最小授权、防重放)、可观测性(request_id、trace、指标)和版本治理(向后兼容、灰度与下线计划),这样接口既能稳定上线也能长期迭代。