1. 为什么你的接口文档总是被前端吐槽
我最近在Review一组内部系统的接口,前端同学甩过来一句话:“这个接口到底是返回数据还是返回错误?为什么200状态码里还包着一层code=500?”我点开代码一看,老项目里十几个接口确实都存在这个问题——状态码永远返回200,真正的业务错误全塞在JSON的code字段里,前端拿数据先得解析三层,再对着一堆魔法数字做判断。
这不是个别现象。很多Python Web开发者从写第一个接口开始,就一直在用“类RPC”的方式设计接口:URL是动词+宾语,比如/get_user_info、/create_order;方法只用POST;状态码只认识200和500;参数校验全靠自己写if-else。这样的接口能用,但如果项目要维护三五年、要对接多个端、要让不同水平的同事协作,问题就会集中爆发。
RESTful API设计不是背Roy Fielding那篇论文,而是一套在工程上验证过的、面向资源的接口组织方式。它的核心思想很简单:把后端能力表达成一套资源集合,用HTTP本身的方法语义去表达对资源的操作,用状态码去表达请求的结果。这套约定本身不复杂,难的是在真实业务里持久地坚持它。
这篇博客定位是给有一定Python Web基础的开发者看的。我不会只堆理论,而是从资源建模、状态码约定、认证权限、代码组织、上线避坑几个维度,把一套能直接落地的RESTful API设计实践完整讲透。我用的框架示例以FastAPI为主,但大多数原则对Django REST Framework、Flask-RESTful同样适用。
现在,我们从最容易走歪的第一步说起。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 资源建模与URL规划:RESTful的地基,也是走样最严重的地方
2.1 先理解“资源”和“动作”的区别
RESTful API里,URL表达的一律是资源,资源的类型是名词,比如用户、订单、文章、评论。对这个资源能做什么,交给HTTP方法去表达,而不是写进URL里。
错误示范:
/api/get_user?id=1—— GET是动词,URL里又出现get/api/delete_order?order_id=2—— 把删除动作写进URL/api/create_order—— 创建这个动作本身不需要出现在URL里
正确示范:
GET /api/users/{id}—— 获取用户POST /api/orders—— 创建订单DELETE /api/orders/{id}—— 删除订单
第一批RESTful初学者最容易困惑的就是:为什么POST /api/orders能表达“创建”这件事?答案是POST方法本身在HTTP语义里就承担了“向指定资源提交数据、由服务器决定创建什么”的职责。你不再需要写一个 create 进去,方法已经把这个语义带了。
2.2 资源命名四个铁律
我在多个项目里强行统一过命名规范,真正能长期执行下去的就这么几条:
全部小写,用连字符分隔单词。 对应Python代码里的user_profile这种下划线命名,在URL里一律改成user-profile。下划线在某些字体下会被下划线修饰遮挡,部分老旧系统解析URL时对下划线也存在兼容性问题,连字符是Web社区事实标准。
用复数名词表示资源集合。 /api/users表示所有用户,/api/users/{id}表示集合中的一个。虽然也有争论说单数更符合语法,但复数写法在工具链生态里占据了绝对主流,翻翻Stripe、GitHub的文档你就能感受到,跟随主流永远是成本最低的选择。
嵌套层级不超过两层。 如果用户有文章,文章有评论,你会本能地写成:
code复制GET /api/users/{user_id}/articles/{article_id}/comments
这种三层嵌套是接口设计的危险信号。一旦超过两层,建议用查询参数或把资源直接提升为顶级资源:
code复制GET /api/articles/{article_id}/comments
GET /api/comments?article_id=123
前一种保留了一层必要的上下文关系,后一种把查询条件放到参数里,语义更轻。选择依据很简单:评论是否必须脱离文章独立存在。如果只在文章详情页用,用嵌套;如果后台系统也要按评论维度管理,用顶级资源加过滤。
不要用动词。 凡是出现/api/users/login、/api/orders/export、/api/users/register这类URL,都要停下来想想能不能用资源表达。登录这个动作本质上是在“创建会话”,写成POST /api/sessions;导出是“生成导出任务”这个资源,写成POST /api/export-tasks。强行动词化是RESTful设计最容易犯的毛病,也最能体现一个人是真正理解了REST还是只学会了表面。
2.3 查询参数:过滤、排序、分页的约定
集合类接口几乎必然要面对三个需求:过滤、排序、分页。设计规范要提前定死,否则每个接口都会有自己的小偏好。
过滤用统一形态,字段名直接映射模型字段:
code复制GET /api/users?status=active&role=admin
GET /api/orders?created_at_gte=2024-01-01&total_lte=1000
日期过滤建议用_gte、_lte、_gt、_lt后缀表示范围,这比单独定义start_date、end_date两个参数更通用,而且可以组合出更丰富的范围条件。
排序统一用sort参数,正序用字段名,倒序加负号:
code复制GET /api/users?sort=-created_at
GET /api/products?sort=-price,name
分页这里重点说一下。业界主流有两种方案:offset/limit和cursor。
offset/limit适合数据量不大的管理后台,实现简单,跳页方便。但它有个致命问题——深分页性能差。OFFSET 100000意味着数据库要扫描并丢弃前100000行。
cursor分页适合C端对性能要求高的场景,它用created_at或id这类有序字段作为游标,只取WHERE id > cursor的记录,性能恒定。缺点是无法直接跳页,只能一页页往下翻。
我建议的技术选型是:后台管理系统用offset/limit,面向用户端Feed流用cursor。分页响应结构也要统一,避免每个接口返回的元信息字段都不一样:
json复制{
"items": [...],
"pagination": {
"total": 1000,
"offset": 0,
"limit": 20
}
}
cursor分页对应结构则把total去掉,因为总数统计在大数据集下代价很高,改为next_cursor:
json复制{
"items": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTAwfQ",
"has_more": true
}
}
这里有个常被忽略的细节:分页对象和业务数据要分离,不要混在数组里。前端拿到一个顶层对象,从items取列表,从pagination取分页信息,后续扩展字段不会破坏已有数据结构。
3. HTTP方法语义与状态码:装懂还是真懂,一测便知
3.1 四个方法的语义边界
HTTP方法在RESTful设计中有明确的分工,合理的项目90%的接口只需要用到以下四个:
| 方法 | 语义 | 典型场景 | 是否幂等 |
|---|---|---|---|
| GET | 读取资源,无副作用 | 查详情、列表 | 是 |
| POST | 创建资源,或执行无法归类的复杂操作 | 新建订单、上传文件 | 否 |
| PUT | 整体替换资源 | 更新用户全部信息 | 是 |
| PATCH | 局部更新资源 | 只修改用户手机号 | 否 |
| DELETE | 删除资源 | 删除评论 | 是 |
幂等是理解方法语义的关键。幂等意味着“同一个请求重复执行多次,结果和执行一次相同”。PUT /api/users/1完整替换用户信息,执行三次结果一致。但POST /api/orders每次执行都会创建一个新订单,这就是非幂等。
实际项目里我总是提醒团队:更新操作优先用PATCH而不是PUT。原因很现实——前端传PUT请求时往往只带了修改的字段,如果后端用整个对象接收,没传的字段就会被置为空。PATCH天然表达“部分更新”,配合exclude_unset这类机制,能精准只更新传入字段。
3.2 状态码不是随便填的数字
状态码是HTTP协议给调用方的第一层反馈,它应当在网络层就告诉调用方请求的大致结果。一个设计良好的API,前端可能只需要判断一个response.ok就能决定走成功还是失败分支,而不是把状态码一律设为200再在body里猜。
我常用的状态码选型:
200 OK:GET查询成功、PUT/PATCH更新成功201 Created:POST创建成功,响应头Location带新资源地址204 No Content:DELETE删除成功,或更新成功但无需返回body400 Bad Request:参数错误、校验不通过401 Unauthorized:未认证,比如token缺失或过期403 Forbidden:已认证但无权访问404 Not Found:资源不存在,或URL不存在409 Conflict:资源状态冲突,比如删除一个已被引用的分类422 Unprocessable Entity:语义错误,比如JSON格式合法但字段类型不对429 Too Many Requests:触发限流500 Internal Server Error:服务端未捕获异常503 Service Unavailable:服务熔断或依赖不可用
有几个边界确认过无数次,写在这里:
删除接口返回200还是204? 我统一用204。删除成功后不需要返回被删对象,返回204告诉调用方“已经没了,别等了”。如果客户端需要知道删除前的数据,应该由客户端在删除前自己调GET。
校验错误用400还是422? 我习惯这样区分:400是请求格式层面的问题,比如缺少必填参数、JSON解析失败;422是“请求格式没问题但语义不合法”,比如年龄字段传了字符串"18"。FastAPI的RequestValidationError默认就是422,所以使用FastAPI时422会高频出现。这个区分对前端的意义在于:前端拿到400,可以定位是请求构造的问题;拿到422,说明是参数值本身不对,需要提示用户修改输入。
认证失败和权限不足必须区分开。 401和403混用是我在联调时见过最多的低级错误。401告诉你“你是谁”,403告诉你“你能干什么”。Token过期返回401,前端收到后自动跳登录页;普通用户访问管理员接口返回403,前端提示无权限。如果两者混成一个,前端就得靠解析业务码才能决定跳转逻辑,状态码的唯一价值就丢了。
3.3 统一错误结构
状态码表达了结果类别,但还不够。调用方需要知道具体哪里错了,因此在错误响应体上也要有统一的结构约定。我长期用的结构是这个:
json复制{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"details": {
"field": "user_id",
"value": "abc123"
}
}
}
关键设计点:
code是机器可读的错误码,前端可以用它做分支判断,不受文案变动影响message是人类可读的说明,可以随语言环境或版本变化details承载附加信息,字段校验错误时可以放具体字段名和非法值
关于错误码,很多人爱用纯数字:code: 10001。这在你一个人维护时没问题,但协作项目里,数字是无意义的,每次前端拿一个10001都要去翻文档。我更推荐用带前缀的字符串错误码,例如ORDER_ALREADY_PAID、FILE_TOO_LARGE,可读性高,也减少了文档对照成本。
3.4 一个接口多种状态码,不算“设计不干净”
有同事跟我说过:“我的接口返回的只有200和500,多简洁。”这不是简洁,是信息缺失。一个POST /api/orders设计得当的话,天然会面对这些情况:
- 参数不对:400
- 用户未登录:401
- 库存不足:409
- 创建成功:201
每个状态码都对应一种真实业务分支。前端可以根据状态码直接决定UI反馈,不需要再解析业务码去猜。把状态码收敛成两种的新手思维,其实是在给自己挖坑。
4. 认证、权限与幂等:生产环境绕不开的三道坎
4.1 认证方案选型:JWT还是Session
RESTful API最常用的认证方案是JWT,核心流程是:客户端登录后,服务端签发一个包含用户标识和过期时间的签名Token,客户端后续请求在Authorization头带上Bearer <token>,服务端验签后即可识别用户。
JWT最大的好处是服务端无状态,不需要在内存或数据库里存Session,水平扩容时不需要考虑会话同步问题。但无状态也有代价:Token签发后无法主动失效,除非引入黑名单机制。黑名单机制的本质是把Token重新变成有状态,这抵消了JWT部分优势。
我的建议是:
- 小型项目、前后端分离、不需要服务端主动踢人,直接用JWT
- 大型系统、需要精细的会话管理,用Session Redis方案或混用:认证走Session,业务资源走JWT
无论选哪种,都要注意以下实现细节,这些都是被安全问题教育过之后才加上的:
- Token存放在
Authorization头,不放URL参数,避免被日志和浏览历史泄露 - 密钥长度至少256位,且轮换有保障
- Token有效期根据业务定,短一点更安全,配合RefreshToken机制让用户无感续期
4.2 权限控制:角色放中间件,归属在资源里
权限控制一定要分层。第一层是身份认证,确认“你是谁”;第二层是角色判断,确认“你属于什么角色”;第三层是资源归属判断,确认“这张订单是不是你的”。
在FastAPI里我用依赖注入做这件事,代码结构清晰了很多:
python复制def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer())
) -> User:
token = credentials.credentials
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
user = db.get_user(payload["sub"])
if user is None:
raise HTTPException(status_code=401, detail="用户不存在或已被禁用")
return user
def require_roles(*roles):
def checker(user: User = Depends(get_current_user)):
if user.role not in roles:
raise HTTPException(status_code=403, detail="无权限访问")
return user
return checker
角色判断只解决“能不能访问这个接口”的问题。跨用户数据访问是另一回事——普通用户不能操作别人家的订单。这个必须做资源级校验,在业务代码里对归属字段进行显式判断,不能只依赖角色。
python复制@app.delete("/api/orders/{order_id}")
def delete_order(order_id: int, user=Depends(get_current_user)):
order = db.get_order(order_id)
if order is None:
raise HTTPException(status_code=404, detail="订单不存在")
if order.user_id != user.id and user.role != "admin":
raise HTTPException(status_code=403, detail="无权操作该订单")
db.delete_order(order)
return Response(status_code=204)
权限控制还有一个常被忽略的维度:数据可见性。列表接口经常遇到的问题就是,本应只看自己的用户,因为后端没加过滤条件,能看到全量数据。我建议列表查询函数里强制注入当前用户的ID作为过滤条件的一部分,而不是把当前用户ID交给前端传递。后端永远不要信任前端传入的身份相关参数。
4.3 幂等性设计:为POST接口上一份保险
POST天然非幂等,这在网络重试和用户重复点击时会出问题。支付、下单、创建文件这类操作,重复提交的代价是致命的。
方案一:幂等键(Idempotency Key)
客户端在请求头传一个唯一ID,服务端用这个ID做去重:
python复制IDEMPOTENCY_TTL = 24 * 60 * 60
def create_order_with_idempotency(
payload: OrderCreate,
idempotency_key: str = Header(..., alias="Idempotency-Key"),
):
existing = redis.get(f"idem:{idempotency_key}")
if existing:
return JSONResponse(content=json.loads(existing), status_code=200)
order = create_order(payload)
redis.setex(
f"idem:{idempotency_key}",
IDEMPOTENCY_TTL,
json.dumps({"order_id": order.id}),
)
return JSONResponse(content={"order_id": order.id}, status_code=201)
这个方案的关键在幂等键的生成和管理由客户端负责。前端在下单页进入时生成一个UUID,点击提交按钮携带同一UUID重试,服务端天然去重。
方案二:数据库唯一约束
在订单表上用业务唯一键加约束,比如user_id + request_id做联合唯一索引,数据库层面拒绝重复插入。这个方案在分布式场景比Redis锁更可靠,因为数据库是最终一致性的基石。建议两个方案配合使用:接口层做幂等键缓存,数据层加唯一约束兜底。
5. 从接口定义到代码落地:给Python后端的三条工程建议
5.1 用类型和Schema把接口契约钉死
RESTful API的很多联调问题,根源在于“接口契约”只存在于口头和文档里,代码层面没有强制约束。Python的动态语言特性让这个问题更严重——函数签名不定义类型,返回结构靠字典随意拼。
解决方案是全面引入Pydantic模型。请求体、路径参数、查询参数、响应体全部用模型定义,让类型和结构在代码层面成为硬约束。
python复制class UserCreate(BaseModel):
username: str = Field(min_length=3, max_length=32, pattern=r"^[a-zA-Z0-9_]+$")
email: EmailStr
password: str = Field(min_length=8, max_length=128)
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
created_at: datetime
class UserListResponse(BaseModel):
items: list[UserResponse]
pagination: Pagination
这样做有四个直接收益:
校验前置。字段类型错误、值超出范围、格式不合法,全部在进入业务函数前被拦截。业务代码里再也不用写if "email" not in data这种防御代码。
文档自动生成。FastAPI会从模型推断出OpenAPI文档,前端同事可以直接在/docs页面看到请求体和响应体的结构,连字段含义注释都能带过去。
返回结构调整有章可循。所有响应都经过统一模型,不会出现这个接口返回user_name、那个接口返回username的命名漂移。
IDE提示友好。使用Pydantic模型后,IDE能识别字段类型,自动补全和错误提示都有效。
5.2 路由组织按资源模块划分,不按函数堆积
Python Web项目常见的路由组织问题,是全部路由堆在一个文件里,或者按照函数类型划分(把所有GET放一个文件、所有POST放一个文件)。这两种都不利于维护。正确思路是按资源模块划分。
code复制app/
main.py
routers/
users.py
orders.py
articles.py
comments.py
models/
schemas/
services/
每个路由模块负责一个资源的所有操作:
python复制# app/routers/orders.py
from fastapi import APIRouter, Depends
router = APIRouter(prefix="/api/orders", tags=["订单"])
@router.get("", response_model=OrderListResponse)
def list_orders(...): ...
@router.post("", response_model=OrderResponse, status_code=201)
def create_order(...): ...
@router.get("/{order_id}", response_model=OrderResponse)
def get_order(...): ...
@router.patch("/{order_id}", response_model=OrderResponse)
def update_order(...): ...
@router.delete("/{order_id}", status_code=204)
def delete_order(...): ...
这种组织方式的好处非常直观:要加订单的某个功能,直接去orders.py,所有相关接口都在一个文件里,代码导航效率成倍提升。APIRouter统一设置prefix后,子接口URL不会重复造轮子。
有一点要注意:列表接口和详情接口的URL规划。列表是GET /api/orders,详情是GET /api/orders/{id},同名路径不同的路径长度。不要写成GET /api/orders/all或GET /api/orders/list,那会破坏资源的层级关系。
5.3 响应字段的版本演进策略
接口上线后一定会遇到字段变更。小改加字段没问题,但删除字段或改变字段类型就是破坏性变更。现实业务里不可能做到永远只加不改,所以要有策略。
我推荐的最小迁移策略:新增字段直接加,删除字段先废弃后删除,变更类型则新建字段。
json复制{
"user_name": "张三",
"display_name": "张三",
"deprecated_user_name": "张三"
}
第一步是添加display_name,保留user_name三个发布周期;第二步前端全部切换后,把user_name移除。这个过程的本质是给调用方留出迁移窗口,避免改一个字段导致全端不可用。
如果有多个客户端(小程序、App、Web)且版本控制混乱,要考虑在URL或请求头中引入版本号。我的建议是:只在破坏性变更时升版本,URL用/api/v1/这种简单直白的方式。太多人为了“未来可能变更”预先设计好版本机制,结果版本号形同虚设,还增加了URL长度和维护成本。务实一点,等真的要破坏性变更了,再动版本也不迟。
6. 接口上线之前,把该踩的坑提前踩完
6.1 高频联调冲突点:那些“我也没想到”的接口问题
接口交付后,前端同事来问的问题往往集中在几类。提前把这些都在设计阶段想清楚,能减少很多来回沟通。
日期时间格式。我用datetime类型时,FastAPI默认序列化出来是ISO 8601格式,比如2024-01-15T10:30:00Z。但很多前端同事默认会用new Date(...)去解析,时区差点搞错。建议统一使用带时区的ISO 8601格式,并在文档里明确标注“所有时间均为UTC”。
枚举值的表现方式。订单状态status字段,是返回字符串"paid"还是数字1?我强烈建议返回字符串。数字状态码需要前端对照文档才能看懂,字符串自解释。如果担心存储效率和索引,那是数据库层面的事,接口层永远面向可读性。
空值的处理。一个用户没有手机号,phone字段返回null、空字符串还是省略字段?三种处理方式前端处理逻辑完全不同。我统一用null,并且用response_model保证字段始终存在,前端可以放心地用user.phone === null做判断。
布尔值的命名。字段叫is_active、is_deleted还是active、deleted?每个项目混着来会让前端摸索很久。选定一种风格就全文统一,我偏好带is_前缀,因为可读性更高,也符合Python布尔变量的常见命名习惯。
6.2 性能上的三个隐形杀手
RESTful API的性能问题,很多不在业务代码而在设计习惯。
列表接口的字段胖瘦问题。 一个订单列表接口如果返回订单里所有关联商品的完整信息,列表页的响应体就会暴涨。建议列表接口只返回卡片页需要的最小字段集,详情接口返回全量字段。我常用两套response_model来管理这种差异:
python复制class OrderSummary(BaseModel):
id: int
order_no: str
total_amount: Decimal
status: str
class OrderDetail(OrderSummary):
items: list[OrderItem]
address: Address
payment: PaymentInfo | None
关联对象的序列化时机。 用ORM时,最容易踩的坑是在序列化阶段懒加载数据库。序列化订单时去查用户、查地址、查商品,每个订单都要多发好几条SQL,列表接口几百条记录直接把数据库拖垮。解决办法:查询阶段用selectinload或joinedload做好预加载,一次查询把关联数据都取回来。这个问题不解决,响应时间会随数据增长线性恶化。
分页上限保护。 对limit参数不做约束,前端或者调用方传一个limit=100000,就是把数据库当内存用。我通常在参数校验层限制分页大小上限:
python复制@router.get("")
def list_orders(
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
):
这样即使在管理后台,单次响应也不会超过100条,从根上堵住超大数据量拉取。
6.3 文档、Mock与联调:把接口契约前置
很多团队接口联调效率低,是因为后端还在开发,前端拿不着接口。加快这个流程的做法是基于OpenAPI文档先做Mock。
FastAPI天然自带Swagger UI,后端把路由、请求模型、响应模型定义好,文档就已经生成。前端可以直接在Swagger UI上看参数结构,也可以把OpenAPI JSON导入到Apifox、Postman这类工具,生成Mock服务。这样前端可以完全并行开发,不用等后端代码跑起来。
联调阶段我的习惯是:
- 要求后端同事的接口文档与代码同步更新,凡是改了字段没改文档,统一按Bug处理
- 要求接口必须能在Swagger UI上直接试通,试不通的接口不开始联调
- 要求每个接口有明确的正常返回和异常返回示例,前端可以据此准备所有分支
这套流程跑顺之后,前后端联调的时间能缩短一半以上,很多问题在设计阶段就已经暴露,而不是等代码都写完了才发现接口契约对不上。
6.4 使用FastAPI落地时的具体避坑笔记
用FastAPI做过几个生产项目之后,有几个细节是文档里不会强调、但真实场景必踩的。
路由顺序坑。 FastAPI按路由定义顺序匹配。如果先定义了GET /api/users/{user_id},再定义GET /api/users/me,那么/api/users/me会被{user_id}捕获,把"me"当作user_id去解析,然后报错。解决方法是:把静态路径放在动态路径前面。我习惯把所有/me、/current这种静态路径写在资源路由的最前面。
Query参数类型坑。 查询参数在URL里全是字符串,Pydantic会自动做类型转换。但bool类型的转换有个经典坑:?is_active=0在部分版本里会被转成True还是False的行为存在差异。建议布尔Query参数只用true和false两个值,不要在文档里让前端传0/1。
响应模型和返回数据不一致时,FastAPI会过滤字段。 当response_model定义好了,返回的字典里多了字段,会被静默丢弃;少了字段,如果字段没有默认值会报错。这个机制帮我拦住了很多结构不一致的Bug,但也会让排查变得隐蔽。我的排查方式是:遇到“接口返回和预期不一致”的问题,先对比response_model和实际返回结构,八成问题都在这里。
全局异常处理要兜底。 即使代码写得再规范,总有漏网之鱼。配置一个全局异常处理器,把未捕获异常统一转成500结构,同时记录日志,防止FastAPI默认的纯文本错误直接泄漏给调用方。
python复制@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
logger.error("Unhandled exception", exc_info=exc)
return JSONResponse(
status_code=500,
content={"error": {"code": "INTERNAL_ERROR", "message": "服务器内部错误"}},
)
这里有个权衡:即便我煞费苦心把内部结构统一了,返回的细节仍然要克制。日志里记录完整堆栈,给调用方只暴露一个通用的500信息,不要在响应里带内部字段名或SQL信息。
7. 结尾的几条心里话
接口设计这件事,看起来是“怎么写URL”的小事,实际上是整个团队协作效率的底层协议。我见过太多项目因为接口设计没章法,导致前端代码里充斥着针对每个接口的特判、后端的中间件越叠越厚、联调时间被无意义拉长。这些问题都不是“多写两行代码”能解决的,是设计阶段就埋下的债。
如果只能从这篇博客带走三句话,我希望是:URL老老实实表达资源,状态码老老实实表达结果,错误结构老老实实给全信息。剩下的认证、幂等、分页、命名统一,都是在这三个地基上叠砖。
我在实际项目中最后还会做一个动作:把接口规范整理成一份团队文档,并把关键规则写进代码评审Checklist。没有规范的项目走不远,有了规范不执行的团队走更偏。规范本身要在设计阶段由后端牵头定下来,但要让前端参与评审——毕竟接口的一半使用方是他们,契约的体验权理应双方共享。
