前言:你是不是也经常懵?
刚开始学FastAPI的时候,我每次写接口都要纠结半天:
- 查数据用GET还是POST?
- 更新数据用PUT还是POST?它们有啥区别?
- 删除资源到底用DELETE还是POST?
相信很多同学都有同样的困惑。网上很多教程一上来就贴代码,讲完语法却不告诉你什么场景该用什么。这篇文章换个思路——先讲场景,再上代码,最后一张表总结,看完你就彻底清楚了。
一、核心概念:四个接口到底什么关系?
GET、POST、PUT、DELETE是四种HTTP方法,对应数据的增删改查(CRUD)操作。
打个比方——把你的服务器想象成一个仓库管理员:
| HTTP方法 | 类比 | 回答的问题 | 对应CRUD |
|---|---|---|---|
| GET | 去仓库查货 | "这个货架上有多少件货?" | Read(查) |
| POST | 往仓库存新货 | "给我新增一个货架,放这批货" | Create(增) |
| PUT | 把仓库的货整个换掉 | "把3号货架上的货全部清空,换成这批新货" | Update(改) |
| DELETE | 从仓库销毁货物 | "把3号货架上的货扔掉" | Delete(删) |
一句话总结:
GET只读不改,POST新增创建,PUT整体替换,DELETE删除资源。
下面逐个拆解:
二、GET接口:只管查,不管改
什么时候用GET?
- 获取资源列表或详情
- 搜索、筛选、分页查询
- 不修改服务器上的任何数据
GET请求的核心特征是安全且幂等——安全意味着不产生副作用,幂等意味着调用1次和调用100次结果一样。
完整代码示例
from fastapi import FastAPI, Query, HTTPException from pydantic import BaseModel app = FastAPI() # 模拟数据库 fake_users = { 1: {"id": 1, "name": "张三", "age": 25, "email": "zhangsan@test.com"}, 2: {"id": 2, "name": "李四", "age": 30, "email": "lisi@test.com"}, } class UserOut(BaseModel): id: int name: str age: int email: str # 场景1:获取所有用户(支持分页查询) @app.get("/users/", response_model=list[UserOut]) async def get_users(skip: int = 0, limit: int = Query(10, le=100)): """查询用户列表,skip和limit通过URL查询参数传递""" users = list(fake_users.values()) return users[skip : skip + limit] # 场景2:获取单个用户详情 @app.get("/users/{user_id}", response_model=UserOut) async def get_user(user_id: int): """通过路径参数获取指定用户""" if user_id not in fake_users: raise HTTPException(status_code=404, detail="用户不存在") return fake_users[user_id] # 场景3:按关键词搜索用户 @app.get("/users/search/", response_model=list[UserOut]) async def search_users( keyword: str = Query(..., min_length=1, description="搜索关键词"), max_age: int = Query(default=100, le=150, description="年龄上限"), ): """通过查询参数搜索用户""" results = [ u for u in fake_users.values() if keyword in u["name"] and u["age"] <= max_age ] return results关键点
| 特征 | 说明 |
|---|---|
| 参数位置 | 路径参数/users/{id}或查询参数?skip=0&limit=10 |
| 请求体 | 不能有请求体(GET请求不携带body) |
| 幂等性 | 幂等——多次调用结果相同 |
| 安全性 | 安全——不修改服务器数据 |
| 缓存 | 浏览器/CDN可以缓存GET响应 |
| 使用场景 | 查列表、查详情、搜索、筛选 |
注意路由顺序:
/users/search/必须定义在/users/{user_id}之前,否则FastAPI会把search当成user_id来匹配。
三、POST接口:创建新东西
什么时候用POST?
- 创建新资源(注册用户、新增文章、提交订单)
- 提交表单数据
- 执行一个非幂等的操作(同样的请求提交两次会创建两条记录)
POST的核心特征是不幂等——提交两次相同的数据,会创建两个资源。
完整代码示例
from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field app = FastAPI() fake_users = { 1: {"id": 1, "name": "张三", "age": 25, "email": "zhangsan@test.com"}, } next_id = 2 # 请求模型——客户端提交的数据 class UserCreate(BaseModel): name: str = Field(..., min_length=1, max_length=50, description="用户名") age: int = Field(..., ge=0, le=150, description="年龄") email: str = Field(..., description="邮箱地址") # 响应模型——返回给客户端的数据 class UserOut(BaseModel): id: int name: str age: int email: str @app.post("/users/", response_model=UserOut, status_code=201) async def create_user(user: UserCreate): """创建新用户,数据通过请求体(body)传递""" global next_id # 检查邮箱是否重复 for existing in fake_users.values(): if existing["email"] == user.email: raise HTTPException(status_code=400, detail="邮箱已被注册") # 存入"数据库" new_user = { "id": next_id, "name": user.name, "age": user.age, "email": user.email, } fake_users[next_id] = new_user next_id += 1 return new_user关键点
| 特征 | 说明 |
|---|---|
| 参数位置 | 请求体(body),用Pydantic模型接收 |
| 幂等性 | 不幂等——重复提交会创建多个资源 |
| 请求体 | 必须有请求体(通常JSON格式) |
| 安全性 | 不安全——会修改服务器数据 |
| 使用场景 | 创建资源、提交表单、上传文件 |
四、PUT接口:整体替换
什么时候用PUT?
- 完整更新一个资源(把旧数据整体替换成新数据)
- 创建一个已知ID的资源(如果不存在就创建,存在就覆盖)
PUT的核心特征是幂等——对同一个资源用相同的数据调用1次和100次,最终状态完全一样。因为PUT是"整体替换",替换成同样的内容,结果不变。
PUT vs POST:最容易搞混的这对
| 对比维度 | POST | PUT |
|---|---|---|
| 语义 | 创建新资源 | 替换/更新已有资源 |
| 幂等性 | 不幂等(调两次创建两条) | 幂等(调两次结果一样) |
| 谁决定ID | 服务器决定(服务器分配新ID) | 客户端决定(URL中指定ID) |
| 典型URL | POST /users/ | PUT /users/{id} |
完整代码示例
from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field app = FastAPI() fake_users = { 1: {"id": 1, "name": "张三", "age": 25, "email": "zhangsan@test.com"}, 2: {"id": 2, "name": "李四", "age": 30, "email": "lisi@test.com"}, } class UserUpdate(BaseModel): """PUT请求模型——所有字段必须提供,因为是整体替换""" name: str = Field(..., min_length=1, max_length=50, description="用户名") age: int = Field(..., ge=0, le=150, description="年龄") email: str = Field(..., description="邮箱地址") class UserOut(BaseModel): id: int name: str age: int email: str @app.put("/users/{user_id}", response_model=UserOut) async def update_user(user_id: int, user: UserUpdate): """PUT:整体替换用户信息 客户端必须提供所有字段。 如果某个字段没传,PUT会把它覆盖为None或报错。 """ if user_id not in fake_users: raise HTTPException(status_code=404, detail="用户不存在") # 整体替换——所有字段都用新值覆盖 fake_users[user_id] = { "id": user_id, "name": user.name, "age": user.age, "email": user.email, } return fake_users[user_id]PUT的"整体替换"到底是什么意思?
假设数据库中有个用户:
{"id": 1, "name": "张三", "age": 25, "email": "zhangsan@test.com"}你只想改名字,用PUT发送:
{"name": "张三丰"}结果:age和email会被覆盖掉!因为PUT的语义是"整体替换",你没提供的字段会被清空。
如果你只想改名字,应该提供所有字段:
{"name": "张三丰", "age": 25, "email": "zhangsan@test.com"}补充:如果你只想改一个字段、不想传所有字段,那应该用PATCH方法(局部更新)。FastAPI中用
@app.patch(),请求模型中所有字段设为Optional。本文主要讲四种核心方法,PATCH道理类似。
关键点
| 特征 | 说明 |
|---|---|
| 参数位置 | 路径参数指定资源 + 请求体提供新数据 |
| 幂等性 | 幂等——相同数据多次调用结果一致 |
| 请求体 | 必须有请求体(完整的资源数据) |
| 核心语义 | 整体替换,客户端必须提供所有字段 |
| 使用场景 | 完整更新资源、Upsert(存在则更新,不存在则创建) |
五、DELETE接口:删东西
什么时候用DELETE?
- 删除指定资源(删除用户、删除文章、删除订单)
- 取消订阅、注销账号
DELETE的核心特征是幂等——删除同一个资源,不管调1次还是100次,最终状态都是"已删除"。
完整代码示例
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() fake_users = { 1: {"id": 1, "name": "张三", "age": 25, "email": "zhangsan@test.com"}, 2: {"id": 2, "name": "李四", "age": 30, "email": "lisi@test.com"}, } class DeleteResult(BaseModel): success: bool message: str deleted_id: int @app.delete("/users/{user_id}", response_model=DeleteResult) async def delete_user(user_id: int): """通过路径参数指定要删除的用户""" if user_id not in fake_users: raise HTTPException(status_code=404, detail="用户不存在") deleted_user = fake_users.pop(user_id) return DeleteResult( success=True, message=f"用户 {deleted_user['name']} 已删除", deleted_id=user_id, )关键点
| 特征 | 说明 |
|---|---|
| 参数位置 | 通常用路径参数/users/{id}指定要删除的资源 |
| 请求体 | 一般不需要请求体(但FastAPI允许DELETE带body) |
| 幂等性 | 幂等——删除已删除的资源,结果还是"不存在" |
| 安全性 | 不安全——会修改服务器数据 |
| 使用场景 | 删除资源、注销、取消 |
常见疑问:删除操作要不要返回数据?两种做法都可以:返回被删除的资源信息,或者只返回一个状态信息(如上面的
DeleteResult)。RESTful规范没有强制要求,团队统一即可。
六、一张表总结:什么时候用什么
| 维度 | GET | POST | PUT | DELETE |
|---|---|---|---|---|
| 核心用途 | 查询数据 | 创建数据 | 整体更新数据 | 删除数据 |
| 对应CRUD | Read(查) | Create(增) | Update(改) | Delete(删) |
| 参数位置 | 路径+查询参数 | 请求体(body) | 路径参数+请求体 | 路径参数 |
| 请求体 | 不能有 | 必须有 | 必须有(完整数据) | 通常不需要 |
| 幂等性 | 幂等 | 不幂等 | 幂等 | 幂等 |
| 安全性 | 安全(无副作用) | 不安全 | 不安全 | 不安全 |
| 会修改数据 | 不会 | 会 | 会 | 会 |
| 谁决定ID | 不涉及 | 服务器决定 | 客户端指定(URL中) | 客户端指定(URL中) |
| 典型URL | GET /users/{id} | POST /users/ | PUT /users/{id} | DELETE /users/{id} |
| 典型状态码 | 200 | 201 Created | 200 | 200或204 No Content |
速记口诀:
GET查不改,POST建不幂等,PUT全替换,DELETE删了算。
七、完整实战:四个接口串起来
下面是一个完整的用户管理接口,四种方法全部用到:
from fastapi import FastAPI, HTTPException, Query from pydantic import BaseModel, Field from typing import Optional app = FastAPI(title="用户管理API") # 模拟数据库 db = { 1: {"id": 1, "name": "张三", "age": 25, "email": "zhangsan@test.com"}, } next_id = 2 # ========== 数据模型 ========== class UserCreate(BaseModel): """POST创建用户的请求模型""" name: str = Field(..., min_length=1, max_length=50) age: int = Field(..., ge=0, le=150) email: str = Field(..., description="邮箱地址") class UserUpdate(BaseModel): """PUT更新用户的请求模型——所有字段必须提供""" name: str = Field(..., min_length=1, max_length=50) age: int = Field(..., ge=0, le=150) email: str = Field(..., description="邮箱地址") class UserOut(BaseModel): """对外响应模型""" id: int name: str age: int email: str class DeleteResult(BaseModel): """删除操作响应模型""" success: bool message: str deleted_id: int # ========== 接口实现 ========== # 1. GET:查询用户列表 @app.get("/users/", response_model=list[UserOut], summary="获取用户列表") async def list_users(skip: int = 0, limit: int = Query(10, le=100)): users = list(db.values()) return users[skip : skip + limit] # 2. GET:查询单个用户 @app.get("/users/{user_id}", response_model=UserOut, summary="获取用户详情") async def get_user(user_id: int): if user_id not in db: raise HTTPException(status_code=404, detail="用户不存在") return db[user_id] # 3. POST:创建新用户 @app.post("/users/", response_model=UserOut, status_code=201, summary="创建用户") async def create_user(user: UserCreate): global next_id for u in db.values(): if u["email"] == user.email: raise HTTPException(status_code=400, detail="邮箱已被注册") new_user = { "id": next_id, "name": user.name, "age": user.age, "email": user.email, } db[next_id] = new_user next_id += 1 return new_user # 4. PUT:整体更新用户 @app.put("/users/{user_id}", response_model=UserOut, summary="更新用户") async def update_user(user_id: int, user: UserUpdate): if user_id not in db: raise HTTPException(status_code=404, detail="用户不存在") db[user_id] = { "id": user_id, "name": user.name, "age": user.age, "email": user.email, } return db[user_id] # 5. DELETE:删除用户 @app.delete("/users/{user_id}", response_model=DeleteResult, summary="删除用户") async def delete_user(user_id: int): if user_id not in db: raise HTTPException(status_code=404, detail="用户不存在") deleted = db.pop(user_id) return DeleteResult( success=True, message=f"用户 {deleted['name']} 已删除", deleted_id=user_id, )运行方式:
pip install fastapi uvicorn uvicorn main:app --reload # 打开 http://127.0.0.1:8000/docs 即可在Swagger UI中测试 #或者下载Apifox中调式八、常见踩坑总结
坑1:用POST做查询
有些同学习惯全部用POST,觉得方便。但这破坏了HTTP语义,导致浏览器无法缓存结果、CDN无法缓存、API语义混乱。查询就用GET,创建就用POST,别偷懒。
坑2:搞混PUT和POST
最经典的混淆:你想更新一个用户,却用了POST。
POST /users/1 → 语义上是"在/users/1下创建新资源",不是更新 PUT /users/1 → 语义上是"替换/users/1这个资源",这才是更新记住:URL中有具体ID + 要修改数据 → 用PUT;URL中无ID + 要创建数据 → 用POST。
坑3:PUT只传了部分字段
# 数据库中:{"id": 1, "name": "张三", "age": 25, "email": "zhangsan@test.com"} # 你只想改名字,用PUT只传了name: PUT /users/1 {"name": "张三丰"} # 结果:age和email被覆盖为None!因为PUT是"整体替换",没传的字段会被清掉正确做法:PUT请求必须携带所有字段。如果只想改一个字段,用PATCH(@app.patch())。
坑4:路由顺序写反了
# 错误顺序:/users/{user_id} 会先匹配到 /users/search @app.get("/users/{user_id}") async def get_user(user_id: int): ... @app.get("/users/search") async def search_users(): ... # 永远到不了这里! # 正确顺序:固定路径放前面 @app.get("/users/search") async def search_users(): ... @app.get("/users/{user_id}") async def get_user(user_id: int): ...坑5:把PUT当成"局部更新"
PUT的语义是整体替换,不是局部更新。只想改部分字段应该用PATCH:
class UserPatch(BaseModel): """PATCH请求模型——所有字段可选""" name: Optional[str] = None age: Optional[int] = None email: Optional[str] = None @app.patch("/users/{user_id}", response_model=UserOut) async def patch_user(user_id: int, user: UserPatch): """PATCH:局部更新,只改传了的字段""" if user_id not in db: raise HTTPException(status_code=404, detail="用户不存在") stored = db[user_id] update_data = user.model_dump(exclude_unset=True) stored.update(update_data) return stored九、总结
回到开头的问题——什么时候用什么接口?
- 查数据→ GET,参数放URL,不修改数据
- 建数据→ POST,参数放body,服务器分配ID
- 改数据→ PUT,参数放URL+body,整体替换
- 删数据→ DELETE,参数放URL,删完就没了
记住这个对应关系,90%的场景都能覆盖。剩下的特殊情况(PATCH局部更新)原理类似,举一反三即可。
FastAPI的设计理念就是用类型注解把一切自动化——你声明好模型,验证、过滤、文档全部自动生成。把GET/POST/PUT/DELETE用好,API设计就是一件很享受的事。
如果这篇文章对你有帮助,欢迎点赞收藏!有问题可以在评论区交流,我会一一回复。