Skip to content

如何设计一个接口

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):同一请求执行一次或多次,最终效果一致(典型 PUTDELETE)。
方法 常见用途 是否安全 是否幂等 典型返回
GET 查询资源 200,无资源可 404
POST 创建或触发动作 否(可通过幂等键实现“业务幂等”) 创建可 201,异步可 202
PUT 全量替换资源 200204
PATCH 部分更新资源 语义上不一定 200204
DELETE 删除资源 204(重复删除仍可 204

3.3 状态码与错误语义:不要“永远 200”

状态码承载的是协议语义,业务错误应配合结构化错误体表达。常见映射如下:

场景 推荐状态码 说明
参数校验失败 400 请求不合法,通常不该重试
未认证 401 缺 token 或 token 无效
无权限 403 已认证但无权限
资源不存在 404 对外接口注意避免枚举攻击泄露存在性
并发写冲突 409 乐观锁冲突,提示重试或刷新
频率限制 429 配合 Retry-After
服务端异常 500 服务 bug 或未捕获异常
上游超时或不可用 502503504 网关/代理更常见

3.4 REST 与 RPC 的对比取舍

选择接口风格时,面试不需要“站队”,要能讲清楚约束与治理成本:

维度 REST(HTTP + JSON) RPC(gRPC/自研协议)
语义表达 借助资源与方法语义,天然适合 CRUD 更像函数调用,表达灵活
治理与网关 生态成熟(鉴权、限流、缓存、观测) 需要更强的平台能力配套
性能 文本协议开销更大 二进制更省,适合高吞吐
兼容演进 JSON 兼容依赖客户端容错 Protobuf 需遵守 tag 演进规则
对外开放 更容易被第三方理解与调试 一般更偏内部服务调用

4. 请求与响应契约(Contract)

4.1 字段设计:命名、类型、默认值

字段契约要做到“看一眼就不误用”:

  • 命名:统一风格(如 snake_casecamelCase),不要一会 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 无状态不等于“不可控”,需要配合 expjti、黑名单或 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-ControlETag,命中后返回 304 Not Modified

避免“所有查询都打 DB”,否则扩容成本会线性上升。

7.2 同步与异步:长耗时接口不要硬扛

长耗时操作(导出、批处理、复杂计算)建议改为异步模型:

  1. POST /exports 返回 202 + task_id
  2. GET /exports/{task_id} 查询状态。
  3. 完成后回调 webhook 或提供下载链接。

这样可以避免线程长期占用,提升系统吞吐与稳定性。

7.3 批量接口:明确“部分成功”的语义

批量写入常见陷阱是“部分成功”。需要明确策略:

  • 全有或全无:事务化,失败整体回滚,响应更简单。
  • 部分成功:每个 item 返回独立结果与错误码,客户端可重试失败项。

部分成功建议返回结构化结果,避免靠字符串解析。

7.4 降级与保护:让接口在高峰期可控

先保护系统,再保护功能

  • 资源隔离:线程池隔离、连接池隔离,避免慢接口拖垮快接口。
  • 限流:令牌桶或漏桶,按租户或用户维度治理。
  • 熔断:下游异常时快速失败,避免级联超时。

8. 可观测性与可运维

8.1 关联标识:请求级与业务级都要有

至少要能做到:

  • 请求级:request_id(网关生成或透传)。
  • 链路级:W3C Trace Context(如 traceparent)用于分布式追踪。
  • 业务级:order_idpayment_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、指标)和版本治理(向后兼容、灰度与下线计划),这样接口既能稳定上线也能长期迭代。