AI学习-RESTful API 系统性学习记录
RESTful API 学习摘要 RESTful API是一种基于资源的架构风格,核心是通过HTTP方法(GET/POST/PUT/DELETE等)统一操作资源(名词化URL)。相比传统自定义接口(如/getUser),它具有六大架构约束: 客户端-服务器分离 无状态(每个请求独立) 可缓存(明确缓存策略) 统一接口(资源标识+标准操作) 分层系统(中间层透明) 按需代码(可选) 实际开发中,应规
RESTful API 系统性学习记录
学习起点
你已经会写 FastAPI 路由了:
@app.get("/api/items")
@app.post("/api/items")
@app.get("/api/items/{item_id}")
问题是:为什么这样写就是"RESTful"的?换成 /getItems、/createItem 为什么不行?
这个疑问引出一整条学习链路:REST 是什么 → 6 大约束 → HTTP 方法的正确用法 → URL 命名规范 → 状态码 → 分页/过滤/排序 → 错误处理 → 版本控制 → 认证授权。
一、REST 到底是什么
1.1 一句话定义
REST(Representational State Transfer,表现层状态转移) 是一种架构风格,不是协议也不是标准。
Roy Fielding 在 2000 年的博士论文中提出了它。简单理解:
REST 教你:把服务器的功能看作一组"资源",然后让客户端用统一的方式去操作这些资源。
1.2 没有 REST 之前的世界
# ❌ 非 RESTful:每个操作自定义一个 URL
/getUserList # 查询
/createUser # 新增
/deleteUser?id=1 # 删除
/updateUser # 更新
/getUserBlogList?id=1 # 查用户博客
/createBlog # 创建博客
问题:
- URL 命名全凭个人习惯,团队协作成本高
- 前端要记一堆接口名
- 新增一个功能就得加一个 URL
1.3 RESTful 的解法
# ✅ RESTful:用 HTTP 方法 + 名词资源
GET /users # 查
POST /users # 增
DELETE /users/1 # 删
PUT /users/1 # 改
GET /users/1/blogs # 查用户的博客
核心转变:前端只需要知道资源名(users、blogs),然后根据想做什么操作,选对应的 HTTP 方法。
二、REST 的 6 大约束(面试必考)
这是 RESTful 的"定义",符合这些约束的接口才叫 RESTful。面试官问"什么是 RESTful",本质上就是问这 6 条。
2.1 客户端-服务端(Client-Server)
客户端(前端) 服务端(后端)
┌──────────┐ HTTP 请求 ┌──────────┐
│ UI/展示 │ ───────────────→ │ 数据/逻辑 │
│ 不关心数据 │ ←─────────────── │ 不关心 UI │
└──────────┘ 响应 └──────────┘
作用:前后端分离,各自独立演进。前端换框架(React → Vue)不影响后端,后端换语言(Python → Go)也不影响前端。
2.2 无状态(Stateless)
服务端不保存客户端状态,每个请求都是独立的。
# ❌ 有状态:服务端记住了"你已经登录了"
POST /login 用户名:admin, 密码:123
→ 服务端创建 session,存起来
GET /users/me
→ 服务端查 session,知道你是谁
# ✅ 无状态:每个请求都带身份信息
POST /login 用户名:admin, 密码:123
→ 返回 token(令牌)
GET /users/me Authorization: Bearer xxxx
→ 服务端解析 token,知道你是谁,不需要查 session
为什么重要?
- 水平扩展容易——加机器就行,不用同步 session
- 容错性好——某台服务器挂了,请求发给别的服务器照样处理
2.3 可缓存(Cacheable)
响应要明确声明能不能缓存、缓存多久。
# FastAPI 中控制缓存
from fastapi.responses import JSONResponse
@app.get("/api/items")
async def list_items():
response = JSONResponse(content=list(items_db.values()))
response.headers["Cache-Control"] = "public, max-age=60" # 缓存 60 秒
# 或设置 ETag(资源指纹,内容变了指纹就变)
response.headers["ETag"] = '"abc123"'
return response
2.4 统一接口(Uniform Interface)
这是 RESTful 最核心的一条,又包含 4 个子要求:
| 子要求 | 含义 | 你的代码 |
|---|---|---|
| 资源标识 | URL 唯一标识一个资源 | /api/items/1 |
| 操作资源 | 通过资源的"表现"操作资源 | 返回 JSON 就是对资源的操作 |
| 自描述消息 | 请求/响应包含足够信息 | 方法+路径+头部+状态码 |
| HATEOAS | 响应里带上可做的下一步操作 | 见下方说明 |
HATEOAS 详解(面试装逼利器):
// 纯 RESTful 的响应应该这样
{
"id": 1,
"name": "笔记本电脑",
"links": [
{"rel": "self", "href": "/api/items/1"},
{"rel": "update", "href": "/api/items/1", "method": "PUT"},
{"rel": "delete", "href": "/api/items/1", "method": "DELETE"}
]
}
现实:99% 的公司项目不实现 HATEOAS。知道概念就够了。
2.5 分层系统(Layered System)
客户端
↓
负载均衡 ← 缓存层
↓
API 网关 ← 认证、限流
↓
微服务 A 微服务 B
↓
数据库
客户端不知道它直接连的是什么,也不知道中间有多少层。每一层只跟相邻层通信。
2.6 按需代码(Code on Demand)——可选
服务器可以把代码(如 JavaScript)发送给客户端执行。这是 REST 中唯一可选的约束,Web 开发中常见,但 API 开发中很少用。
三、核心实战:HTTP 方法
3.1 标准用法速查
| 方法 | 含义 | 是否幂等 | 请求体 | 响应状态码 |
|---|---|---|---|---|
GET |
查询 | ✅ 是 | 无 | 200 |
POST |
新增 | ❌ 否 | 有 | 201 |
PUT |
整体替换 | ✅ 是 | 有 | 200/204 |
PATCH |
局部更新 | ❌ 不一定 | 有 | 200 |
DELETE |
删除 | ✅ 是 | 无 | 204 |
3.2 详细介绍
GET — 查
# GET 是幂等的:查 1 次和查 100 次,服务器状态不变
@app.get("/api/items")
async def list_items():
"""获取所有商品"""
return list(items_db.values())
@app.get("/api/items/{item_id}")
async def read_item(item_id: int):
"""获取单个商品"""
if item_id not in items_db:
raise HTTPException(status_code=404)
return items_db[item_id]
POST — 增
# POST 不是幂等的:POST 一次创建 1 个,POST 两次创建 2 个
@app.post("/api/items", status_code=201)
async def create_item(item: Item):
"""创建商品"""
global next_id
new_id = next_id
next_id += 1
items_db[new_id] = item.model_dump()
return {"id": new_id, **item.model_dump()}
PUT — 整体替换
# PUT 是幂等的:传同样的数据 PUT 多次,结果一样
@app.put("/api/items/{item_id}")
async def replace_item(item_id: int, item: Item):
"""整体替换商品(必须传全部字段)"""
if item_id not in items_db:
raise HTTPException(status_code=404)
# PUT 要求传所有字段,缺失的字段会设为默认值
items_db[item_id] = item.model_dump()
return {"id": item_id, **items_db[item_id]}
PATCH — 局部更新
# PATCH 允许只传要修改的字段
class ItemPatch(BaseModel):
name: Optional[str] = None
price: Optional[float] = None
description: Optional[str] = None
stock: Optional[int] = None
@app.patch("/api/items/{item_id}")
async def update_item(item_id: int, item: ItemPatch):
"""局部更新商品(只传要改的字段)"""
if item_id not in items_db:
raise HTTPException(status_code=404)
# 只更新传了的字段
update_data = item.model_dump(exclude_unset=True)
items_db[item_id].update(update_data)
return {"id": item_id, **items_db[item_id]}
DELETE — 删
# DELETE 是幂等的:删第 1 次返回 204,删第 2 次(已不存在)也返回 204
@app.delete("/api/items/{item_id}", status_code=204)
async def delete_item(item_id: int):
"""删除商品"""
if item_id not in items_db:
# 资源不存在也是"删除成功"(幂等性要求)
return
del items_db[item_id]
3.3 幂等性详解
定义:同一个请求执行多次和执行一次,结果一样。
# GET 幂等:重复查询不改变数据
GET /api/items/1 # 第1次 → {name: "笔记本", price: 5999}
GET /api/items/1 # 第100次 → {name: "笔记本", price: 5999}
# PUT 幂等:同样的数据重复替换
PUT /api/items/1 {name: "笔记本", price: 5999}
PUT /api/items/1 {name: "笔记本", price: 5999} # 结果一样
# DELETE 幂等:已删除的资源再删也是成功
DELETE /api/items/1 → 204
DELETE /api/items/1 → 204(即使已不存在)
# POST 非幂等:每次创建新的
POST /api/items {name: "鼠标"} → 创建 id=4
POST /api/items {name: "鼠标"} → 创建 id=5
为什么 PATCH 不一定幂等?
# 幂等的 PATCH
PATCH /api/items/1 {"price": 4999} # 设价格为 4999
PATCH /api/items/1 {"price": 4999} # 再执行还是 4999 ✅
# 非幂等的 PATCH
PATCH /api/items/1 {"price": "price + 100"} # 加价 100
PATCH /api/items/1 {"price": "price + 100"} # 再加 100 → 结果不同 ❌
四、URL 设计规范
4.1 黄金法则
✅ 用名词,不用动词
✅ 用复数
✅ 用层级关系表示资源归属
✅ 用 query string 做筛选/排序/分页
❌ 不用动词
❌ 不用下划线(用连字符 -)
❌ 不用大写
❌ URL 末尾不加斜杠
4.2 常见模式
| 功能 | 写法 |
|---|---|
| 获取用户列表 | GET /users |
| 获取用户详情 | GET /users/{id} |
| 创建用户 | POST /users |
| 更新用户 | PUT /users/{id} |
| 删除用户 | DELETE /users/{id} |
| 获取用户的博客 | GET /users/{id}/blogs |
| 获取博客的评论 | GET /blogs/{id}/comments |
| 过滤 | GET /users?role=admin |
| 排序 | GET /users?sort=created_at&order=desc |
| 分页 | GET /users?page=1&size=20 |
| 搜索 | GET /users?q=关键字 |
| 字段选择 | GET /users?fields=id,name,email |
4.3 特殊情况处理
批量操作
# 批量获取
GET /api/items?ids=1,2,3
# 批量删除(有争议)
DELETE /api/items?ids=1,2,3
# 或用 POST(不符合纯 RESTful,但实际常用)
POST /api/items/batch-delete body: {"ids": [1, 2, 3]}
业务操作(非 CRUD)
# 方案一:把操作看成某个属性的更新
# ❌ POST /api/articles/1/publish
# ✅ PATCH /api/articles/1 body: {"status": "published"}
# 方案二:作为子资源操作
POST /api/articles/1/publish # 在纯 REST 角度有争议,但实际常用
POST /api/articles/1/like # 同上
DELETE /api/articles/1/like # 取消赞
五、状态码
5.1 速查表
| 类别 | 状态码 | 含义 | 什么时候用 |
|---|---|---|---|
| 2xx 成功 | 200 OK | 请求成功 | GET 查到、PUT/PATCH 更新成功 |
| 201 Created | 创建成功 | POST 创建资源后 | |
| 204 No Content | 成功无返回体 | DELETE 成功后 | |
| 3xx 重定向 | 301 Moved Permanently | 永久重定向 | URL 变了,客户端以后用新 URL |
| 304 Not Modified | 资源没变 | 配合 ETag 做缓存 | |
| 4xx 客户端错误 | 400 Bad Request | 参数错误 | 必填字段缺失、格式错误 |
| 401 Unauthorized | 未认证 | 没登录、token 过期 | |
| 403 Forbidden | 无权限 | 已登录但没权限 | |
| 404 Not Found | 资源不存在 | URL/ID 不存在 | |
| 405 Method Not Allowed | 方法不允许 | 资源不支持该 HTTP 方法 | |
| 409 Conflict | 冲突 | 重复创建、数据版本冲突 | |
| 422 Unprocessable Entity | 校验失败 | 请求体格式对但语义不对 | |
| 429 Too Many Requests | 限流 | 请求太频繁 | |
| 5xx 服务端错误 | 500 Internal Server Error | 服务端内部错误 | 代码崩溃 |
| 502 Bad Gateway | 网关错误 | 上游服务挂了 | |
| 503 Service Unavailable | 服务不可用 | 正在重启/过载 |
5.2 代码示例
# 200 — 查询成功
@app.get("/api/items")
async def list_items():
return list(items_db.values()) # FastAPI 自动序列化为 JSON,默认 200
# 201 — 创建成功
@app.post("/api/items", status_code=201)
async def create_item(item: Item):
...
# 204 — 删除成功,不返回内容
@app.delete("/api/items/{item_id}", status_code=204)
async def delete_item(item_id: int):
...
# 400 — 请求参数错误
@app.get("/api/items")
async def list_items(page: int = 1, size: int = 20):
if size > 100:
raise HTTPException(
status_code=400,
detail="单页数量不能超过 100"
)
# 401 — 未认证(通常通过中间件统一处理)
# 403 — 无权限
# 404 — 资源不存在
@app.get("/api/items/{item_id}")
async def read_item(item_id: int):
if item_id not in items_db:
raise HTTPException(
status_code=404,
detail="商品不存在"
)
六、统一的错误响应
6.1 为什么要统一
// ❌ 不统一的错误(前端处理起来想骂人)
{
"error": "商品不存在" // 有的是字符串
}
{
"code": 404, "msg": "not found" // 有的字段名不同
}
// 空着只返回 404 状态码 // 有的什么都没
6.2 正确的做法
# 统一错误响应模型
class ErrorResponse(BaseModel):
code: str # 业务错误码
message: str # 可读的错误信息
detail: Optional[str] = None # 详细调试信息
# 在 FastAPI 中统一异常处理
from fastapi import Request
from fastapi.responses import JSONResponse
class AppException(Exception):
def __init__(self, status_code: int, code: str, message: str, detail: str = None):
self.status_code = status_code
self.code = code
self.message = message
self.detail = detail
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"detail": exc.detail,
}
},
)
# 使用
raise AppException(404, "ITEM_NOT_FOUND", "商品不存在", f"id={item_id} 未找到")
// 统一后的响应
{
"error": {
"code": "ITEM_NOT_FOUND",
"message": "商品不存在",
"detail": "id=123 未找到"
}
}
七、分页、过滤、排序
7.1 标准实现
from typing import Optional
@app.get("/api/items")
async def list_items(
# 分页
page: int = 1,
size: int = 20,
# 排序
sort: str = "created_at",
order: str = "desc",
# 过滤
category: Optional[str] = None,
min_price: Optional[float] = None,
max_price: Optional[float] = None,
# 搜索
q: Optional[str] = None,
):
# 模拟:实际代码从数据库查询
items = list(items_db.values())
# 过滤
if category:
items = [i for i in items if i.get("category") == category]
if min_price:
items = [i for i in items if i["price"] >= min_price]
# 模拟分页
total = len(items)
start = (page - 1) * size
end = start + size
page_items = items[start:end]
# 返回时包含分页信息
return {
"data": page_items,
"pagination": {
"page": page,
"size": size,
"total": total,
"total_pages": (total + size - 1) // size,
},
}
7.2 响应格式
{
"data": [
{"id": 1, "name": "笔记本电脑", "price": 5999},
{"id": 2, "name": "机械键盘", "price": 299}
],
"pagination": {
"page": 1,
"size": 20,
"total": 2,
"total_pages": 1
}
}
八、版本控制
8.1 为什么需要版本
当你改了接口格式(比如返回字段变了),旧版本的前端(已经发布的 APP)要用旧格式,新版本的前端用新格式。你需要同时支持两种格式。
8.2 常见方案
# 方案一:URL 路径(最常用)
# /api/v1/items → 旧版本
# /api/v2/items → 新版本
app_v1 = FastAPI()
app_v2 = FastAPI()
@app_v1.get("/api/v1/items")
async def list_items_v1():
return [{"name": i["name"]} for i in items_db.values()]
@app_v2.get("/api/v2/items")
async def list_items_v2():
# 新版本返回更多字段
return list(items_db.values())
# 方案二:请求头(更优雅,但调试不方便)
# Accept: application/vnd.company.v1+json
@app.get("/api/items")
async def list_items():
# 根据请求头返回不同版本
...
九、RESTful + FastAPI 完整项目结构
"""
一个完整的 RESTful API 项目结构示例
app/
├── main.py # FastAPI 应用入口
├── models/ # Pydantic 模型
│ ├── item.py # Item, ItemCreate, ItemUpdate
│ └── common.py # 通用模型(分页、错误等)
├── routers/ # 路由
│ ├── items.py # /api/items
│ └── users.py # /api/users
├── services/ # 业务逻辑
│ └── item_service.py
├── db/ # 数据库
│ └── database.py
└── middleware/ # 中间件(认证、日志等)
└── auth.py
文件:routers/items.py
from fastapi import APIRouter, HTTPException, Depends
from models.item import Item, ItemCreate, ItemUpdate
from typing import Optional
router = APIRouter(prefix="/api/items", tags=["商品"])
@router.get("")
async def list_items(
page: int = 1,
size: int = 20,
category: Optional[str] = None,
):
"""获取商品列表"""
...
@router.get("/{item_id}")
async def read_item(item_id: int):
"""获取单个商品"""
...
@router.post("", status_code=201)
async def create_item(item: ItemCreate):
"""创建商品"""
...
@router.put("/{item_id}")
async def replace_item(item_id: int, item: ItemCreate):
"""整体替换商品"""
...
@router.patch("/{item_id}")
async def update_item(item_id: int, item: ItemUpdate):
"""局部更新商品"""
...
@router.delete("/{item_id}", status_code=204)
async def delete_item(item_id: int):
"""删除商品"""
...
十、常见面试题深度解析
10.1 RESTful 和 HTTP 的关系
HTTP 是传输协议 → 负责把数据从 A 传到 B
REST 是架构风格 → 规定数据传过去之后怎么表示、怎么操作
类比:
HTTP = 公路(运输介质)
REST = 交通规则(靠右行驶、红灯停绿灯行)
10.2 RESTful 和 GraphQL 对比
| 维度 | RESTful | GraphQL |
|---|---|---|
| 数据获取 | 服务端定好返回格式 | 客户端自己选字段 |
| 请求数 | 可能需要多次请求 | 一次请求搞定所有数据 |
| 学习成本 | 低 | 中高 |
| 缓存 | 天然 HTTP 缓存 | 需自己实现 |
| 适合场景 | 公开 API、资源型接口 | 复杂关联数据、前端频繁变动的场景 |
| 定位 | 主流标准,所有平台通用 | 特定场景的工具,数据聚合场景优秀 |
10.3 RESTful 接口如何做认证?
# 最常见的方案:JWT(JSON Web Token)
# 登录:用户名密码 → 返回 token
POST /api/auth/login
body: {"username": "admin", "password": "123456"}
→ {"token": "eyJhbGciOiJIUzI1NiIs..."}
# 后续请求在 Header 中携带 token
GET /api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# FastAPI 中统一验证
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
@app.get("/api/users/me")
async def get_me(credentials: HTTPAuthorizationCredentials = Depends(security)):
token = credentials.credentials
# 验证 token,返回用户信息
10.4 RESTful 的性能优化
- 缓存:正确设置 Cache-Control、ETag
- 压缩:启用 Gzip
- 分页:控制单次返回量
- 字段选择:
?fields=id,name,price - 批量接口:一次请求处理多数据
- 异步处理:耗时操作放后台,立即返回"处理中"
十一、常见反模式(避坑指南)
❌ 反模式 1:把动词放 URL
# 错
@app.get("/getItems")
@app.post("/createItem")
@app.get("/deleteItem")
# 对
@app.get("/items")
@app.post("/items")
@app.delete("/items/{id}")
❌ 反模式 2:全部用 POST
# 错(很多人图省事这么干)
@app.post("/getItems")
@app.post("/deleteItem")
@app.post("/updateItem")
# 对
GET /items
DELETE /items/{id}
PUT /items/{id}
❌ 反模式 3:URL 用动词
# 错
GET /articles/published-articles
POST /articles/publish
# 对
GET /articles?status=published
PATCH /articles/{id} body: {"status": "published"}
❌ 反模式 4:忽略状态码
# 错:全部返回 200
return {"code": 404, "message": "not found"} # 状态码还是 200
# 对
raise HTTPException(status_code=404)
❌ 反模式 5:多层嵌套
# 错:嵌套超过 3 层,URL 又臭又长
GET /schools/{id}/classes/{id}/students/{id}/grades/{id}
# 对:超过 2 层时,考虑用 query 参数简化
GET /grades?student_id=123&class_id=456
❌ 反模式 6:请求体里放 GET 参数
# 错:GET 请求带 body(虽然 HTTP 允许,但不该这么用)
@app.get("/api/search")
async def search(body: SearchBody): # GET 应该用 query 参数,不是 body
...
# 对
@app.get("/api/search")
async def search(q: str, category: str = None):
...
十二、系统性思考总结
12.1 一个 RESTful 请求的完整生命周期
前端(Vue/React)调用 GET /api/items?page=1&size=20
↓
浏览器构建 HTTP 请求(方法 + URL + 头部)
↓
[Uvicorn] 监听端口,收到请求
↓
[Uvicorn] 解析 HTTP → 传给 FastAPI
↓
[FastAPI] 匹配路由 → /api/items → list_items()
↓
[FastAPI] Pydantic 自动校验参数 page=1, size=20
↓
[业务逻辑] 查数据库 → 过滤 → 分页 → 组装结果
↓
[FastAPI] 返回 JSON → 自动设置状态码 200
↓
前端收到响应,渲染页面
12.2 四个核心认知
1. RESTful 是规范,不是技术——你不需要学一种叫"RESTful"的语言
2. 核心就一句话:URL 表示"对什么操作",HTTP 方法表示"怎么操作"
3. RESTful 的好处不在代码层面——你写 /getItems 也能跑
它的好处在团队协作层面——所有人接口风格统一,降低沟通成本
4. 面试考的不是背诵——是考察你有没有"设计接口"的意识
12.3 学习路径
阶段一:理解 RESTful 6 大约束(读这篇就够了)
↓
阶段二:在你的 FastAPI 项目中有意识地使用正确方法
↓
阶段三:看完 https://fastapi.tiangolo.com/tutorial/
↓
阶段四:读阮一峰《RESTful API 设计指南》
↓
阶段五:多看开源项目的接口设计(GitHub 搜 rest-api 相关项目)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)