先聊个实际场景。团队最近要启动一个新项目,后端由你负责,前端同事跑过来问:“接口文档什么时候出?参数长什么样?出错怎么提示?”如果你只是丢过去一份随手写的路由清单,那接下来联调阶段的沟通成本一定会让你怀疑人生。反过来,如果你能在动手敲代码之前,把“接口应该长什么样”这件事想清楚,前后端协作能顺畅一大截。这篇博文想说的就是这件事:在Python技术栈下,怎么把RESTful API设计得既规范又实用。
我见过太多接口设计问题:URL里动词满天飞、删除操作用GET、所有接口无论对错一律返回200、错误提示是“系统错误”四个字……这些问题单个看都不致命,但堆在一起,会让接口变得难以维护、难以测试、难以对接。RESTful API设计的本质,不是给资源起名或者选状态码,而是建立一套前后端都能理解的通用语言。这篇文章适合正在写Python接口的开发者、刚从Flask/Node转过来的同学,以及所有被接口文档逼疯过的前端朋友——我会从设计思路讲起,拆解URL、HTTP方法、状态码、错误处理、认证安全这些核心环节,最后给出基于FastAPI和Django REST Framework的实战示例,以及我踩过的坑。
1. 内容整体设计与思路拆解
1.1 为什么REST风格到今天依然是主流
REST不是最早出现的接口设计风格,也不是最“先进”的,但它依然是目前Web API领域接受度最高的方案。原因在于它抓住了HTTP协议本身的特性:URL用来定位资源,HTTP方法用来表达操作意图,状态码用来反馈处理结果。这套机制任何Web开发者都熟悉,不需要额外封装协议。相比SOAP那种重量级XML封装,或者早期PHP项目里常见的“action.php?method=getUser”这种自定义参数调用,REST把接口的语义暴露在HTTP层本身,让浏览器、代理、缓存、监控系统都能直接理解你的API在做什么。
另一个优势是资源导向的思维模式。拿到一个需求,先想清楚“这里面有哪些资源”,而不是“有哪些操作”,这件事本身就是在梳理业务模型。比如一个电商订单系统,资源是用户、商品、订单、支付记录,操作(下单、支付、退款)本质上是对这些资源的状态迁移——创建订单、变更支付状态、发起退款。用REST的思维建模,接口天然就是稳定的,业务动作再复杂,也能归类到资源的变化上。
当然REST也有争议,比如有人吐槽它处理复杂查询和批量操作时很别扭。这个观点有一定道理,所以我在第5章会讲如何处理分页、过滤、部分更新这些“不太好REST”的场景,以及什么时候可以适度变通。
1.2 设计API首先要回答的三个问题
在写第一行代码之前,我建议先回答三个问题,它们决定了API的整体走向。
第一个问题是:接口的使用者是谁?如果是内部前后端联调,设计可以灵活一点;如果接口要开放给第三方开发者,那规范性和稳定性要求就完全不同。第二个问题是:数据的消费方是浏览器还是服务端?浏览器场景需要考虑CORS、预检请求,服务端场景则不用太纠结。第三个问题是:接口的生命周期会持续多久?如果预期会长期维护并持续迭代,版本策略、兼容性方案必须在设计初期就定下来,否则后面每次改动都是一次事故。
这三个问题听起来很虚,但它们直接影响URL结构、版本管理方式、响应格式的细节。我在实际项目中看到过为了短期方便,把内部逻辑暴露在API里的接口,后来被外部系统依赖,想改都改不掉。API设计本质上是一种“公共契约”,前期多花一小时想清楚,后期能省下几十小时的扯皮。
2. 核心细节解析与实操要点
2.1 资源命名:URL里的名词哲学
资源命名是RESTful API最显性的设计决策。核心原则也就几条:用名词不用动词,用复数形式,小写加连字符,层级不要过深。
理论上正确例子很多,但实际项目中我见过最典型的反面教材是这两种。一种是动词操作化,比如/api/getUserInfo、/api/deleteOrderById,这种风格把RPC调用的思维带进了REST,URL里全是动作,资源的概念完全消失了。另一种是层级灾难,比如/api/users/123/orders/456/products/789/reviews/1001,四层嵌套,看着就头疼。
层级嵌套的本质是什么?它表达的是“从属关系”。user拥有order,order包含product,这是合理的。但层级每加深一层,URL的维护成本就上升一个量级。我的经验是:层级超过两层就该考虑是否真正需要嵌套。如果第三层和前面两层没有强从属关系,干脆平铺——/api/products/789/reviews/1001就比挂三层嵌套清晰得多。
再补充一个命名细节。URL里用连字符(-)而不是下划线(_),因为下划线在部分浏览器和字体排版中会被下划线样式遮挡,影响可读性。另外坚持小写,避免大小写混用造成的歧义。集合资源用复数形式(/users),这已经成为事实标准,虽然严格来说REST规范并不强制复数,但一致性比“正确”更重要。
关于动词类的“特殊动作”怎么处理,有一个实用的变通方式:用子资源的形式承载动作语义,比如POST /users/123/password/reset(重置密码)、POST /orders/456/cancel(取消订单)。这种写法虽然没有严格遵循“资源=名词”的原则,但它能清晰地表达幂等性难以描述的状态迁移,比在URL里塞?action=resetPassword这种参数干净得多。这是我推荐的一种“适度偏离”。
2.2 HTTP方法语义:不只是GET和POST
HTTP方法在REST里是对资源的操作意图,按语义可以分为两类:安全方法(不会改变资源状态)和幂等方法(重复执行结果一致)。这个区分不是理论游戏,它直接影响接口的容错设计——客户端超时重试、消息队列补偿、缓存策略,全都依赖“这个方法重试是否安全”。
以最常见的CRUD操作为例:
| 方法 | 语义 | 是否安全 | 是否幂等 | 典型场景 |
|---|---|---|---|---|
| GET | 查询资源 | 是 | 是 | 获取列表/详情 |
| POST | 创建资源 | 否 | 否 | 新增订单 |
| PUT | 整体替换资源 | 否 | 是 | 更新用户全部字段 |
| PATCH | 局部更新资源 | 否 | 是(取决于实现) | 只改用户昵称 |
| DELETE | 删除资源 | 否 | 是 | 删除评论 |
一个常见的坑是PUT和PATCH的区分。PUT要求客户端把整个资源的所有字段都提交上来,服务端用这份数据整体替换已有资源;PATCH只提交需要修改的字段。如果前端只改了昵称,用PUT请求却只带了{"nickname": "新昵称"},服务端把整个用户记录覆盖掉了,邮箱和手机号全变空——这种事故我见过不止一次。所以设计API时明确声明:更新操作支持PUT还是PATCH,还是两者都支持,不能含糊。
幂等性的生活化类比:PUT和DELETE就像是把一份文件整个替换掉,或者把整个文件夹删除——不管执行一次还是执行一百次,最终文件状态是一样的;POST则像往购物车里加一件商品,多加一次,购物车里的东西就多一件,结果完全不同。理解了这个差别,你自然就能判断什么时候该用POST,什么时候该用PUT。
2.3 状态码:用HTTP状态码说出人话
状态码是HTTP层自带的“反馈机制”,但很多开发者在实际项目中把它用废了。我见过最夸张的接口,无论成功失败全部返回200,然后在响应体里塞一个code: 500。这种做法虽然给前端拦了一层适配,但副作用也非常明显:监控系统无法通过状态码判断接口健康度,日志排查效率极低,API网关限流和重试也没法基于状态码做策略。
RESTful设计强调:HTTP状态码本身就是API响应的一部分。200表示成功,400表示客户端参数错误,401表示未认证,403表示无权限,404表示资源不存在,500表示服务端异常。前端只需要判断状态码,就能确定下一步的逻辑分支,完全不需要再看业务错误码。正确使用常用状态码的场景:
| 状态码 | 含义 | 典型触发场景 |
|---|---|---|
| 200 OK | 请求成功 | GET/PUT获取或更新成功 |
| 201 Created | 资源创建成功 | POST新增数据 |
| 204 No Content | 成功但无返回体 | DELETE删除成功 |
| 400 Bad Request | 请求参数不合法 | 缺少必填字段、格式错误 |
| 401 Unauthorized | 未认证 | 未携带Token或Token过期 |
| 403 Forbidden | 无权限 | 已认证但无权访问该资源 |
| 404 Not Found | 资源不存在 | URL错误或资源已被删除 |
| 409 Conflict | 资源状态冲突 | 唯一索引冲突、版本号冲突 |
| 422 Unprocessable Entity | 语义错误 | 请求格式正确但字段值不合理 |
| 429 Too Many Requests | 触发限流 | 请求频率超过阈值 |
| 500 Internal Server Error | 服务端未处理异常 | 程序bug、数据库异常 |
这里有一个我特别想强调的细节:422和400的区别。400表示请求在“语法/结构”层面就不对,比如JSON格式错误、必填字段缺失;422表示请求结构没问题,但内容在业务语义上不合法,比如年龄字段传了负数、邮箱格式校验不过。区分这两者可以让前端精确对应到校验逻辑的不同阶段。
3. 实操过程与核心环节实现
3.1 基于FastAPI实现一个规范的商品API
讲了半天理论,接下来我完整演示一个基于FastAPI的商品信息API,把前面几章的核心约束落进代码里。选择FastAPI示例是因为它在类型提示、自动文档、数据校验方面非常贴合现代Python开发习惯,代码可读性好,适合作为教学骨架。
# app/main.py from datetime import datetime from typing import Optional from fastapi import FastAPI, HTTPException, Query, status from pydantic import BaseModel, Field app = FastAPI(title="Product API", version="1.0.0") # 内存存储,仅用于示例 products_db = {} id_counter = 0 class ProductCreate(BaseModel): """创建商品的请求体""" name: str = Field(..., min_length=1, max_length=50, description="商品名称") price: float = Field(..., gt=0, description="商品单价(元)") stock: int = Field(0, ge=0, description="库存数量") category: str = Field(..., min_length=1, max_length=20, description="商品分类") class ProductUpdate(BaseModel): """更新商品的请求体(PATCH:所有字段均可选)""" name: Optional[str] = Field(None, min_length=1, max_length=50) price: Optional[float] = Field(None, gt=0) stock: Optional[int] = Field(None, ge=0) category: Optional[str] = Field(None, min_length=1, max_length=20) class ProductOut(BaseModel): """商品响应体,使用model_config开启ORM/字典序列化""" id: int name: str price: float stock: int category: str created_at: datetime model_config = {"from_attributes": True} @app.post("/products", response_model=ProductOut, status_code=status.HTTP_201_CREATED) def create_product(product: ProductCreate): """创建商品""" global id_counter id_counter += 1 product_data = product.model_dump() product_data.update( id=id_counter, created_at=datetime.utcnow(), ) products_db[id_counter] = product_data return product_data @app.get("/products", response_model=list[ProductOut]) def list_products( category: Optional[str] = Query(None, description="按分类过滤"), min_price: Optional[float] = Query(None, description="最低价格"), max_price: Optional[float] = Query(None, description="最高价格"), offset: int = Query(0, ge=0, description="偏移量"), limit: int = Query(10, ge=1, le=100, description="每页数量"), ): """商品列表:支持过滤、分页""" result = list(products_db.values()) if category: result = [p for p in result if p["category"] == category] if min_price is not None: result = [p for p in result if p["price"] >= min_price] if max_price is not None: result = [p for p in result if p["price"] <= max_price] return result[offset : offset + limit] @app.get("/products/{product_id}", response_model=ProductOut) def get_product(product_id: int): """商品详情""" product = products_db.get(product_id) if not product: raise HTTPException(status_code=404, detail=f"商品 {product_id} 不存在") return product @app.patch("/products/{product_id}", response_model=ProductOut) def update_product(product_id: int, update: ProductUpdate): """局部更新商品:只更新请求体中出现字段""" product = products_db.get(product_id) if not product: raise HTTPException(status_code=404, detail=f"商品 {product_id} 不存在") update_data = update.model_dump(exclude_unset=True) if not update_data: raise HTTPException(status_code=400, detail="至少需要提供一个待更新字段") product.update(update_data) return product @app.delete("/products/{product_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_product(product_id: int): """删除商品:成功后返回204无响应体""" if product_id not in products_db: raise HTTPException(status_code=404, detail=f"商品 {product_id} 不存在") del products_db[product_id] return None这段代码覆盖了前面讲到的所有核心点:POST创建返回201、GET查询返回200、PATCH局部更新、DELETE删除返回204、404处理、422由Pydantic自动触发。如果你把这段代码用uvicorn main:app --reload跑起来,访问http://127.0.0.1:8000/docs,会自动生成一份可以直接调试的Swagger文档——这也是FastAPI相比Flask最省心的地方。
3.2 Django REST Framework的对比实现
如果你的项目是基于Django,那DRF(Django REST Framework)是绕不开的选择。DRF的思路和FastAPI不同:FastAPI用类型提示直接定义schema,DRF用Serializer类来定义字段和校验规则。实现同一个商品模型的列表和详情接口,DRF的写法长这样:
# app/serializers.py from rest_framework import serializers from .models import Product class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ["id", "name", "price", "stock", "category", "created_at"] # app/views.py from rest_framework import generics from rest_framework.permissions import IsAuthenticatedOrReadOnly from .models import Product from .serializers import ProductSerializer class ProductListCreateView(generics.ListCreateAPIView): """列表 + 创建,GET返回列表,POST创建资源""" queryset = Product.objects.all() serializer_class = ProductSerializer permission_classes = [IsAuthenticatedOrReadOnly] class ProductDetailView(generics.RetrieveUpdateDestroyAPIView): """详情 + 更新 + 删除,GET/PUT/PATCH/DELETE""" queryset = Product.objects.all() serializer_class = ProductSerializer permission_classes = [IsAuthenticatedOrReadOnly] # app/urls.py from django.urls import path from .views import ProductListCreateView, ProductDetailView urlpatterns = [ path("products/", ProductListCreateView.as_view()), path("products/<int:pk>/", ProductDetailView.as_view()), ]DRF的通用视图(generics)已经把CRUD的代码压缩到了极致,ListCreateAPIView自动实现了列表和创建的接口逻辑,RetrieveUpdateDestroyAPIView自动实现了详情、修改、删除。需要注意的一点是:RetrieveUpdateDestroyAPIView默认同时支持PUT和PATCH两种请求方式。如果只想开放PATCH局部更新,需要显式限定,不然前端用PUT提交不完整数据时,序列化校验会直接报错,给联调制造麻烦。我在项目里通常这样处理:重写update方法,让PUT也走部分更新逻辑,或者干脆在URL路由中把PUT方法禁用掉。
还有一点关于DRF的权限控制,IsAuthenticatedOrReadOnly表示匿名用户只能读取,登录用户才能写操作。这套权限体系是DRF的强项,配合Django自带的后台用户模型直接可用,适合大部分内部管理系统的场景。
3.3 参数细节:过滤、分页、排序与字段裁剪
列表接口是API设计里最容易被忽视、也是后期改动最频繁的部分。请求参数层面的细节如果不在一开始就约定清晰,后面每一个新需求都会大面积改接口。
过滤,我的建议是把过滤条件放在query参数里,不要为了过滤去设计新的URL。比如GET /products?category=手机&min_price=1000,而不是GET /products/category/手机。原因是过滤条件的数量和组合是不可预知的,而URL的路径层级应该是稳定且有限的。用query参数做过滤,增加新的过滤条件只是新增参数,对前端是兼容改动。
分页,主流的方案有两种:offset/limit分页和cursor分页。offset/limit就是?offset=20&limit=10,意思是从第20条开始取10条,实现简单但有两个问题:数据量大时深翻页性能差,且数据在翻页过程中发生变化时会出现重复或遗漏。cursor分页用?cursor=eyJpZCI6MTAwfQ这种不透明游标,性能稳定且结果一致,但实现复杂度更高,前端也不如offset直观。我的建议是:数据量小于一万条的场景直接用offset/limit,数据量大会持续增长(比如订单流水、操作日志)用cursor。FastAPI在这个示例里用offset/limit已经足够了,但如果接入真实数据库,建议用第三方库fastapi-pagination统一处理。
排序,约定?sort=-price,created_at这种格式:以逗号分隔多个排序字段,字段名前加负号表示倒序。这个格式直观且易于解析。不要用?order=desc&by=price这种把排序拆成两个参数的写法,多个排序字段时根本没法表达。
字段裁剪,对应的是 JSON:API 规范中的sparse fieldsets,用?fields=id,name,price让客户端只取需要的字段。这个特性在移动端低带宽场景非常有用,但会显著增加服务端实现复杂度。如果是内部API,我的建议是默认不实现、但响应体不要把无关字段暴露出去。比如商品对象里有个internal_remark(内部备注字段),就别往API响应里塞。
3.4 统一响应结构和错误码:一个必须前置的约定
在进入代码之前,所有参与接口协作的人必须明确一个问题:请求成功时响应体长什么样?请求失败时响应体长什么样?这个约定如果没在项目初期定死,后面对接时会出现各种“我给你数组,你给我对象”“我返回字符串,你解析成对象”的悲剧。
关于成功响应,业界一直有“裸返回”和“包壳”两种风格。裸返回就是直接返回资源对象本身,GET /products/1直接返回商品JSON;包壳就是统一包一层{"code": 0, "data": ..., "message": "success"}。JSON:API 规范建议裸返回,HTTP状态码本身就承担了语义表达;很多国内团队习惯包壳,因为历史原因很多旧系统依赖这一点。我个人建议内部API尽量用裸返回,让响应体结构简单直接;如果一定要包壳,意味着你在HTTP状态码之外又建立了一套并存且经常打架的“业务状态码”体系,维护成本会翻倍。
但无论选择哪种风格,错误响应必须统一格式。一个比较推荐的错误响应体结构是:
{ "error": { "code": "PRODUCT_NOT_FOUND", "message": "商品 123 不存在", "details": { "product_id": 123 } } }code用机器可读的字符串(不是数字码),方便前端根据它做分支逻辑;message是人可读的描述,可以直接展示给用户;details是附加的上下文信息,便于排查问题。注意这里code和 HTTP 状态码不是一回事,HTTP状态码是“传输层”语义,告诉客户端请求整体是成功还是失败;error.code是“业务层”语义,告诉客户端具体是哪一种错误。这两者配合使用,而不是互相替代。
FastAPI里统一错误格式的最简单方式是注册一个全局异常处理器:
from fastapi import FastAPI, Request from fastapi.responses import JSONResponse class BizError(Exception): """业务异常基类""" def __init__(self, code: str, message: str, status_code: int = 400, details: dict | None = None): self.code = code self.message = message self.status_code = status_code self.details = details or {} app = FastAPI() @app.exception_handler(BizError) async def biz_error_handler(request: Request, exc: BizError): return JSONResponse( status_code=exc.status_code, content={ "error": { "code": exc.code, "message": exc.message, "details": exc.details, } }, )这样在任何路由中raise BizError(code="PRODUCT_NOT_FOUND", message="商品不存在", status_code=404),响应体就自动统一了,不需要每个视图函数自己拼错误JSON。这是我在所有FastAPI项目里必加的基础设施。
4. 常见问题与排查技巧实录
4.1 典型问题速查表
光看理论不容易形成直觉,我把实战中高频踩坑的场景整理成了一张速查表,包含症状、原因和解决方案,都是可以照着排查的。
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 前端说“接口报错,但抓不到错误信息” | 服务端返回HTML错误页而不是JSON | 配置全局异常处理,兜底所有500/404都返回统一JSON |
| GET请求偶尔成功偶尔失败 | 请求带了body,代理层丢弃 | GET请求不要携带body;需要复杂查询改用POST /search |
| DELETE返回200还带删除对象JSON | 开发觉得“返回点东西前端好处理” | 约定DELETE统一返回204,前端不解析body,减少未知变数 |
| 同一个接口既返回数组又返回对象 | 之前列表为空时返回[],有数据时返回对象 | 列表接口永远返回数组,即使为空也是空数组[] |
| PATCH更新把没传的字段清空了 | PATCH请求用model_dump()全字段更新 | 使用exclude_unset=True只更新请求中实际出现的字段 |
| 401和403混淆 | 分不清未认证与无权限 | 401表示“你是谁”,403表示“我知道你是谁但你不许进来” |
| 时间字段返回UTC但还是差8小时 | 前端拿UTC时间戳直接显示 | 约定时间统一用UTC+ISO8601格式,前端本地化展示时转时区 |
最后一条特别值得展开。时间格式的坑很隐蔽,服务端存储和在Python中处理时间时用UTC(协调世界时)是最安全的,因为不依赖服务器所在时区;但用户最终看到的时间应该是本地时区的。正确做法是:API层统一返回带时区信息的ISO8601字符串(比如2024-06-15T08:30:00Z),前端拿到后调用浏览器的Date解析并本地区显示。不要返回“2024-06-15 16:30:00”这种没有时区信息的字符串——服务端在上海存的是上海时间,服务端迁移到新加坡后存的就是新加坡时间,数据直接乱套。
4.2 一次真实的联调事故:从状态码到错误码的全链路排查
分享一个我之前在项目中遇到的实际案例。同事A负责用户模块,他写了一个更新用户昵称的接口。前端同事B联调时反馈:“调用接口一直报错,但控制台看网络请求是200。”
排查的时候,我先看了前端代码,发现B是在判断data.code === 0才视为成功,否则弹出data.message。而A写的接口成功了返回{"code": 0, "data": {...}},失败时却返回了HTTP状态码500且响应体是{"detail": "Internal Server Error"}——响应体里没有code和message字段。前端拿到的data.code是undefined,不等于0,所以走入了错误分支,弹出了一个undefined的报错弹窗。
这次事故的核心矛盾就是:后端只用HTTP状态码表达错误,而前端依赖业务code判断结果;两边约定不一致,接口联调自然卡住。后来我们做了一次团队内的接口规范梳理,写了一份《接口响应与错误码约定》,所有接口必须遵循两个要点:一是HTTP状态码必须表达“这个请求整体是成功还是失败”,二是失败时响应体必须包含error.code、error.message,并且message要对用户友好。
这件事给我最大的启发是:接口设计不只是技术问题,更是团队协作的契约问题。后端的性能再好、代码再优雅,只要前端理解的接口语义和后端实现不一致,联调效率就一定是灾难。因此所有核心约定,必须落到文字并沉淀在接口文档里,不能靠口头传承。
4.3 版本管理与接口演进:兼容性策略
接口版本策略也是个经典话题。所谓“版本管理”,不是代码仓库的tag管理,而是对外API契约的版本管理。只要接口被其他系统依赖,你就不能随意破坏契约。
常见的版本策略有三种。URL路径版本:/api/v1/products、/api/v2/products,直观、便于路由和日志排查,是最常用的方式;query参数版本:/api/products?version=1,URL好看但容易被忽略,第三方开发者调试时经常忘记带,不推荐;Header版本:Accept: application/json; version=1,实现上最干净,但调试工具查看麻烦,而且跨域场景可能触发预检请求。
我的建议是:对外公开API用URL路径版本(/api/v1/...),这是目前接受度最高、最容易理解和排查的方案;内部API如果团队管控能力强,可以考虑不加版本号,依靠兼容性约定——只增量添加字段,不删除和修改已有字段语义。
另一个关于兼容性的细节是:对未知字段的处理决策。假设服务端返回的商品对象多了一个price_unit字段,前端不会报错,这是纯增量扩展;但如果前端提交创建商品的请求体里多了一个服务端不认识的字段,服务端应该怎么处理?默认FastAPI的Pydantic模型会忽略未定义字段,这可能导致用户以为字段生效了,实际却没存库。解决方式有两个:严格模式(model_config = {"extra": "forbid"},未知字段直接422报错)或者显式支持。对于创建、更新接口,我倾向用严格模式,宁可报错也不静默丢失数据。这个决策一定要和前端对齐,否则排查数据问题时非常费劲。
4.4 文档、测试与调试:让规范和代码同步落地
说了这么多设计原则,如果规范只存在于文档里,而代码和文档不同步,一切等于零。好在Python生态给了我们不错的自动化方案。
FastAPI自身集成了OpenAPI(Swagger)文档,只要你的类型定义清楚,文档会自动生成并且和代码强同步,不存在“代码改了文档忘更新”的问题。Django REST Framework虽然没有原生OpenAPI支持,但可以通过drf-spectacular库生成。对于Flask来说,flask-smorest和flask-restx是较成熟的扩选方案。在选择框架时,就把“能否自动生成API文档”纳入考量,因为手动维护文档的项目,几乎没有能长期保持文档不过期的。
测试层面,pytest配合fastapi.testclient或 DRF 的APITestCase都是标配。这里我建议每个接口至少要覆盖以下测试场景:正常请求的成功路径(断言HTTP状态码和关键返回字段)、参数校验失败路径(断言400/422和错误码)、认证失效路径(断言401)、资源不存在路径(断言404)。这套“四段式”的接口测试习惯,能用最小的成本把接口契约锁死,后续改动才能放心。
调试工具方面,我个人的习惯是:项目开发阶段用HTTPie比较多,因为命令行短、输出带颜色方便眼检。但接口联调阶段我基本是用Postman或者Apifox,因为可以保存请求记录、配置环境变量、生成文档。还有一个被很多人忽略的调试手段:在FastAPI的docs页面里可以直接做请求测试,对接口单测非常方便,尤其在快速验证某个入参组合时,比打开Postman建请求快得多。
5. 写在最后的个人体会
这篇文章从设计思路一直写到代码落地、联调解