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 # 查用户的博客

核心转变:前端只需要知道资源名usersblogs),然后根据想做什么操作,选对应的 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/1204
DELETE /api/items/1204(即使已不存在)

# 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 的性能优化

  1. 缓存:正确设置 Cache-Control、ETag
  2. 压缩:启用 Gzip
  3. 分页:控制单次返回量
  4. 字段选择?fields=id,name,price
  5. 批量接口:一次请求处理多数据
  6. 异步处理:耗时操作放后台,立即返回"处理中"

十一、常见反模式(避坑指南)

❌ 反模式 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 相关项目)
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐