FastAPI RESTful API实战:从接口规范到优雅设计
前言
这篇是 Python 基础合集的学习笔记,这次整理的是 RESTful API 的设计规范和 FastAPI 实战。
说到这个话题,其实有点小故事。前阵子刚进一家公司实习,第一周就被前端同事"教育"了一顿。为啥?因为我写的接口太乱了------查询用户用 POST,删除订单用 GET,返回的数据格式也是五花八门......前端同事看着我的接口文档,一脸无奈地问:"你这接口,我怎么调?"
后来导师告诉我,团队里大家都遵循 RESTful 规范,让我赶紧补课。你想想,一个团队十几个后端,如果每个人接口风格都不一样,前端不得疯掉?所以 RESTful 不只是个技术规范,更是团队协作的"共同语言"。
本篇会从 RESTful 的核心概念讲起,然后用 FastAPI 一步步实现规范的接口,最后还有完整的实战案例。看完这篇,你也能写出让前端同事"刮目相看"的接口了。
🏠个人主页:山沐与山
文章目录
- [一、RESTful API是什么](#一、RESTful API是什么)
- [1.1 一个"被揍"的故事](#1.1 一个"被揍"的故事)
- [1.2 REST的四个关键词](#1.2 REST的四个关键词)
- [1.3 RESTful API长什么样](#1.3 RESTful API长什么样)
- 二、RESTful设计规范详解
- [2.1 URI设计:用名词不用动词](#2.1 URI设计:用名词不用动词)
- [2.2 HTTP方法:增删改查的正确姿势](#2.2 HTTP方法:增删改查的正确姿势)
- [2.3 状态码:让响应会"说话"](#2.3 状态码:让响应会"说话")
- [2.4 查询参数:分页、过滤、排序](#2.4 查询参数:分页、过滤、排序)
- [2.5 版本控制:新老接口兼容](#2.5 版本控制:新老接口兼容)
- [三、FastAPI实现RESTful API](#三、FastAPI实现RESTful API)
- [3.1 项目初始化](#3.1 项目初始化)
- [3.2 定义数据模型](#3.2 定义数据模型)
- [3.3 实现CRUD接口](#3.3 实现CRUD接口)
- [3.4 统一响应格式](#3.4 统一响应格式)
- 四、进阶技巧
- [4.1 嵌套资源](#4.1 嵌套资源)
- [4.2 非CRUD操作怎么办](#4.2 非CRUD操作怎么办)
- [4.3 错误处理](#4.3 错误处理)
- [4.4 自动生成API文档](#4.4 自动生成API文档)
- 五、完整实战案例:用户管理系统
- 六、常见问题
- 七、总结
一、RESTful API是什么
1.1 一个"被揍"的故事
假设你是刚入职的后端程序员小阿八,负责给前端的阿花提供 API 接口。你兴冲冲地写了一周代码,结果被阿花揍得鼻青脸肿:
你写的接口:
GET /getUserList 获取用户列表
POST /deleteUser?id=123 删除用户
GET /updateUserName 修改用户名
POST /addNewUser 新增用户
阿花看了直摇头:"你这接口风格太乱了,我调用都得猜半天!"
你一脸委屈:接口不是能跑就行吗?
不行!这就是为什么要学 RESTful API。
1.2 REST的四个关键词
REST 全称是 Representational State Transfer,翻译过来叫"表现层状态转移"。听起来很抽象?别急,我拆开来讲:
R - Representational(表现层)
资源可以有不同的"表现形式"。比如同一个用户数据,可以用 JSON 格式返回,也可以用 XML 格式返回。目前 JSON 是主流。
json
// JSON 格式(推荐)
{"id": 1, "name": "张三", "email": "zhangsan@example.com"}
xml
<!-- XML 格式(较少使用) -->
<user>
<id>1</id>
<name>张三</name>
<email>zhangsan@example.com</email>
</user>
S - State(状态)
这里说的是"无状态"。什么意思呢?
- 有状态:你去餐厅吃饭,服务员记得你上次点了鱼皮,这次直接问"还是老样子?"
- 无状态:服务员不记得你是谁,每次都要重新点单
RESTful API 是无状态的------服务器不记录客户端的任何信息,每次请求都是独立的。这样做的好处是:想加多少台服务器都行,任何一台都能处理请求,轻松实现负载均衡。
T - Transfer(转移)
转移是双向的:
GET请求:服务器把资源状态"转移"给客户端POST请求:客户端把新的状态"转移"给服务器
组合起来
REST 是一种软件架构风格,让客户端和服务器通过统一的接口,以无状态的方式互相传递资源的表现层数据。
而 RESTful 就是"充满 REST 风格的",RESTful API 就是符合 REST 架构风格的 API。
注意:
RESTful不是协议,不是标准,不是强制规范,只是一种建议的设计风格。你可以遵循,也可以不遵循。
1.3 RESTful API长什么样
还是用户管理的例子,对比一下:
| 操作 | 不规范的写法 | RESTful写法 |
|---|---|---|
| 获取用户列表 | GET /getUserList |
GET /users |
| 获取单个用户 | GET /getUserById?id=1 |
GET /users/1 |
| 创建用户 | POST /addUser |
POST /users |
| 更新用户 | POST /updateUser |
PUT /users/1 |
| 删除用户 | GET /deleteUser?id=1 |
DELETE /users/1 |
看到没?RESTful 风格的接口:
URI用名词 (users),不用动词(getUser)- 用
HTTP方法 表示操作(GET/POST/PUT/DELETE) - 结构清晰,一眼就能看懂
二、RESTful设计规范详解
2.1 URI设计:用名词不用动词
这是 RESTful 最核心的原则之一。
资源用名词复数
✅ /users 用户列表
✅ /products 商品列表
✅ /orders 订单列表
❌ /getUsers 动词,不推荐
❌ /user 单数,不推荐(除非确实只有一个)
具体资源加ID
✅ /users/123 ID为123的用户
✅ /products/456 ID为456的商品
✅ /orders/789 ID为789的订单
嵌套资源
✅ /users/123/orders 用户123的所有订单
✅ /orders/789/items 订单789的所有商品项
⚠️ 不建议嵌套太深:
❌ /users/123/orders/789/items/1/reviews 太深了!
2.2 HTTP方法:增删改查的正确姿势
| 方法 | 用途 | 是否幂等 | 示例 |
|---|---|---|---|
GET |
查询资源 | 是 | GET /users 获取用户列表 |
POST |
创建资源 | 否 | POST /users 创建新用户 |
PUT |
完整更新资源 | 是 | PUT /users/1 更新用户1的全部信息 |
PATCH |
部分更新资源 | 是 | PATCH /users/1 只更新用户1的某些字段 |
DELETE |
删除资源 | 是 | DELETE /users/1 删除用户1 |
什么是幂等?
幂等就是:同样的请求执行一次和执行多次,效果一样。
GET /users/1:查10次,结果都一样------幂等DELETE /users/1:删第一次成功,再删就404了,但用户1确实没了------幂等POST /users:每次都会创建一个新用户------不幂等
2.3 状态码:让响应会"说话"
HTTP 状态码分为5类,常用的需要记住:
| 状态码 | 含义 | 使用场景 |
|---|---|---|
200 |
OK | 请求成功(通用) |
201 |
Created | 创建资源成功 |
204 |
No Content | 删除成功,无返回内容 |
400 |
Bad Request | 客户端请求参数错误 |
401 |
Unauthorized | 未认证(没登录) |
403 |
Forbidden | 已认证但无权限 |
404 |
Not Found | 资源不存在 |
422 |
Unprocessable Entity | 参数格式正确但语义错误 |
500 |
Internal Server Error | 服务器内部错误 |
前端看到 4xx 就知道是自己参数传错了,看到 5xx 就知道是后端的锅------谁也别想甩锅!
2.4 查询参数:分页、过滤、排序
复杂查询用查询参数(Query Parameters)实现:
分页
GET /users?page=1&size=10
GET /users?offset=0&limit=10
过滤
GET /users?status=active
GET /users?role=admin&status=active
GET /products?price_min=100&price_max=500
排序
GET /users?sort=created_at
GET /users?sort=-created_at # 降序(加减号)
GET /users?sort=name,-created_at # 多字段排序
搜索
GET /users?q=张三
GET /products?keyword=手机
2.5 版本控制:新老接口兼容
接口升级时,为了不影响老用户,可以在 URI 中加版本号:
GET /v1/users # 老版本
GET /v2/users # 新版本
或者放在 Header 里:
GET /users
Accept: application/vnd.myapi.v1+json
推荐用 URI 方式,更直观。
三、FastAPI实现RESTful API
3.1 项目初始化
先安装依赖:
bash
pip install fastapi uvicorn pydantic
创建项目结构:
restful_demo/
├── main.py # 入口文件
├── models.py # 数据模型
├── schemas.py # Pydantic 模型
├── database.py # 模拟数据库
└── routers/
└── users.py # 用户相关路由
3.2 定义数据模型
schemas.py:
python
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
"""用户基础模型"""
name: str = Field(..., min_length=2, max_length=50, description="用户名")
email: EmailStr = Field(..., description="邮箱")
age: Optional[int] = Field(None, ge=0, le=150, description="年龄")
class UserCreate(UserBase):
"""创建用户的请求模型"""
password: str = Field(..., min_length=6, description="密码")
class UserUpdate(BaseModel):
"""更新用户的请求模型(所有字段可选)"""
name: Optional[str] = Field(None, min_length=2, max_length=50)
email: Optional[EmailStr] = None
age: Optional[int] = Field(None, ge=0, le=150)
class UserResponse(UserBase):
"""用户响应模型"""
id: int
is_active: bool = True
created_at: datetime
class Config:
from_attributes = True
class PaginatedResponse(BaseModel):
"""分页响应模型"""
total: int = Field(..., description="总数量")
page: int = Field(..., description="当前页码")
size: int = Field(..., description="每页数量")
items: list = Field(..., description="数据列表")
3.3 实现CRUD接口
database.py(模拟数据库):
python
from datetime import datetime
from typing import Dict, Optional
# 模拟数据库
USERS_DB: Dict[int, dict] = {
1: {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com",
"age": 25,
"password": "hashed_password_1",
"is_active": True,
"created_at": datetime(2024, 1, 1, 10, 0, 0)
},
2: {
"id": 2,
"name": "李四",
"email": "lisi@example.com",
"age": 30,
"password": "hashed_password_2",
"is_active": True,
"created_at": datetime(2024, 1, 15, 14, 30, 0)
}
}
# 自增ID
_next_id = 3
def get_next_id() -> int:
global _next_id
current = _next_id
_next_id += 1
return current
routers/users.py:
python
from fastapi import APIRouter, HTTPException, Query, Path, status
from typing import Optional, List
from datetime import datetime
from schemas import UserCreate, UserUpdate, UserResponse, PaginatedResponse
from database import USERS_DB, get_next_id
router = APIRouter(prefix="/v1/users", tags=["用户管理"])
# ============================================
# GET /users - 获取用户列表
# ============================================
@router.get("", response_model=PaginatedResponse, summary="获取用户列表")
async def get_users(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量"),
name: Optional[str] = Query(None, description="按用户名筛选"),
is_active: Optional[bool] = Query(None, description="按状态筛选"),
sort: Optional[str] = Query(None, description="排序字段,如 name, -created_at")
):
"""
获取用户列表,支持分页、筛选、排序
- **page**: 页码,从1开始
- **size**: 每页数量,默认10,最大100
- **name**: 按用户名模糊搜索
- **is_active**: 按激活状态筛选
- **sort**: 排序,字段前加-表示降序
"""
# 筛选
users = list(USERS_DB.values())
if name:
users = [u for u in users if name.lower() in u["name"].lower()]
if is_active is not None:
users = [u for u in users if u["is_active"] == is_active]
# 排序
if sort:
desc = sort.startswith("-")
field = sort.lstrip("-")
if field in ["name", "created_at", "age"]:
users.sort(key=lambda x: x.get(field, ""), reverse=desc)
# 分页
total = len(users)
start = (page - 1) * size
end = start + size
paginated_users = users[start:end]
return {
"total": total,
"page": page,
"size": size,
"items": paginated_users
}
# ============================================
# GET /users/{user_id} - 获取单个用户
# ============================================
@router.get("/{user_id}", response_model=UserResponse, summary="获取用户详情")
async def get_user(
user_id: int = Path(..., ge=1, description="用户ID")
):
"""根据ID获取用户详情"""
if user_id not in USERS_DB:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"用户 {user_id} 不存在"
)
return USERS_DB[user_id]
# ============================================
# POST /users - 创建用户
# ============================================
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED, summary="创建用户")
async def create_user(user: UserCreate):
"""
创建新用户
- 邮箱不能重复
- 密码会被加密存储(这里简化处理)
"""
# 检查邮箱是否已存在
for existing_user in USERS_DB.values():
if existing_user["email"] == user.email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邮箱已被注册"
)
# 创建用户
user_id = get_next_id()
new_user = {
"id": user_id,
"name": user.name,
"email": user.email,
"age": user.age,
"password": f"hashed_{user.password}", # 实际应该用 bcrypt 加密
"is_active": True,
"created_at": datetime.now()
}
USERS_DB[user_id] = new_user
print(f"[+] 用户创建成功: {user.name} (ID: {user_id})")
return new_user
# ============================================
# PUT /users/{user_id} - 完整更新用户
# ============================================
@router.put("/{user_id}", response_model=UserResponse, summary="完整更新用户")
async def update_user(
user_id: int = Path(..., ge=1),
user: UserCreate = ...
):
"""完整更新用户信息(需要传所有字段)"""
if user_id not in USERS_DB:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"用户 {user_id} 不存在"
)
existing = USERS_DB[user_id]
existing.update({
"name": user.name,
"email": user.email,
"age": user.age,
"password": f"hashed_{user.password}"
})
print(f"[UPDATE] 用户 {user_id} 已更新")
return existing
# ============================================
# PATCH /users/{user_id} - 部分更新用户
# ============================================
@router.patch("/{user_id}", response_model=UserResponse, summary="部分更新用户")
async def partial_update_user(
user_id: int = Path(..., ge=1),
user: UserUpdate = ...
):
"""部分更新用户信息(只传需要更新的字段)"""
if user_id not in USERS_DB:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"用户 {user_id} 不存在"
)
existing = USERS_DB[user_id]
update_data = user.model_dump(exclude_unset=True)
for field, value in update_data.items():
if value is not None:
existing[field] = value
print(f"[PATCH] 用户 {user_id} 部分更新: {list(update_data.keys())}")
return existing
# ============================================
# DELETE /users/{user_id} - 删除用户
# ============================================
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, summary="删除用户")
async def delete_user(
user_id: int = Path(..., ge=1)
):
"""删除用户"""
if user_id not in USERS_DB:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"用户 {user_id} 不存在"
)
deleted = USERS_DB.pop(user_id)
print(f"[x] 用户已删除: {deleted['name']} (ID: {user_id})")
# 204 No Content 不返回任何内容
return None
3.4 统一响应格式
虽然 RESTful 没有强制要求响应格式,但团队统一格式是个好习惯:
python
# schemas.py 添加
from typing import TypeVar, Generic
from pydantic import BaseModel
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
"""统一响应格式"""
code: int = 200
message: str = "success"
data: Optional[T] = None
class ErrorResponse(BaseModel):
"""错误响应格式"""
code: int
message: str
detail: Optional[str] = None
使用示例:
python
from schemas import ApiResponse, UserResponse
@router.get("/{user_id}", response_model=ApiResponse[UserResponse])
async def get_user(user_id: int):
user = USERS_DB.get(user_id)
if not user:
return ApiResponse(code=404, message="用户不存在", data=None)
return ApiResponse(data=user)
四、进阶技巧
4.1 嵌套资源
用户的订单、文章的评论,这些都是嵌套资源:
python
# routers/orders.py
router = APIRouter(prefix="/v1", tags=["订单管理"])
# 获取用户的所有订单
@router.get("/users/{user_id}/orders", summary="获取用户订单列表")
async def get_user_orders(
user_id: int = Path(..., ge=1),
page: int = Query(1, ge=1),
size: int = Query(10, ge=1, le=50)
):
"""获取指定用户的所有订单"""
# 先检查用户是否存在
if user_id not in USERS_DB:
raise HTTPException(status_code=404, detail="用户不存在")
# 查询该用户的订单
user_orders = [o for o in ORDERS_DB.values() if o["user_id"] == user_id]
# 分页处理...
return {"total": len(user_orders), "items": user_orders}
# 获取用户的单个订单
@router.get("/users/{user_id}/orders/{order_id}", summary="获取订单详情")
async def get_user_order(
user_id: int = Path(..., ge=1),
order_id: int = Path(..., ge=1)
):
"""获取用户的某个订单详情"""
order = ORDERS_DB.get(order_id)
if not order or order["user_id"] != user_id:
raise HTTPException(status_code=404, detail="订单不存在")
return order
4.2 非CRUD操作怎么办
有些操作不是标准的增删改查,比如"支付订单"、"激活用户"。怎么设计?
方法一:用动词(不太RESTful,但直观)
POST /orders/123/pay
POST /users/456/activate
方法二:把动作转换成名词(更RESTful)
POST /orders/123/payments # 创建一条支付记录
POST /users/456/activations # 创建一条激活记录
方法三:用 PATCH 修改状态
PATCH /orders/123
Body: {"status": "paid"}
PATCH /users/456
Body: {"is_active": true}
哪种都行,团队统一就好。我个人比较喜欢第三种,用 PATCH 修改状态。
4.3 错误处理
统一的错误处理让 API 更专业:
python
# main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from pydantic import ValidationError
app = FastAPI(title="RESTful API Demo", version="1.0.0")
# 自定义异常
class BusinessException(Exception):
def __init__(self, code: int, message: str):
self.code = code
self.message = message
# 处理业务异常
@app.exception_handler(BusinessException)
async def business_exception_handler(request: Request, exc: BusinessException):
return JSONResponse(
status_code=exc.code,
content={
"code": exc.code,
"message": exc.message,
"path": str(request.url)
}
)
# 处理参数验证异常
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
errors.append({
"field": ".".join(str(x) for x in error["loc"]),
"message": error["msg"]
})
return JSONResponse(
status_code=422,
content={
"code": 422,
"message": "参数验证失败",
"errors": errors
}
)
# 处理 404
@app.exception_handler(404)
async def not_found_handler(request: Request, exc):
return JSONResponse(
status_code=404,
content={
"code": 404,
"message": "资源不存在",
"path": str(request.url)
}
)
4.4 自动生成API文档
FastAPI 自带 Swagger 文档,启动服务后访问:
Swagger UI:http://localhost:8000/docsReDoc:http://localhost:8000/redocOpenAPI JSON:http://localhost:8000/openapi.json
前端拿着这个文档就能直接调用,还能在线测试,省去大量沟通成本。
五、完整实战案例:用户管理系统
把上面的代码整合起来,看完整示例:
main.py:
python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from routers import users
app = FastAPI(
title="用户管理系统 API",
description="一个符合 RESTful 规范的用户管理 API",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# 跨域配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(users.router)
# 健康检查
@app.get("/health", tags=["系统"])
async def health_check():
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
运行服务:
bash
python main.py
# 或者
uvicorn main:app --reload
测试接口:
bash
# 获取用户列表
curl http://localhost:8000/v1/users
# 获取单个用户
curl http://localhost:8000/v1/users/1
# 创建用户
curl -X POST http://localhost:8000/v1/users \
-H "Content-Type: application/json" \
-d '{"name": "王五", "email": "wangwu@example.com", "password": "123456"}'
# 部分更新
curl -X PATCH http://localhost:8000/v1/users/1 \
-H "Content-Type: application/json" \
-d '{"name": "张三丰"}'
# 删除用户
curl -X DELETE http://localhost:8000/v1/users/1
# 分页 + 筛选 + 排序
curl "http://localhost:8000/v1/users?page=1&size=10&is_active=true&sort=-created_at"
六、常见问题
6.1 RESTful一定要严格遵守吗?
不一定。RESTful 只是建议的风格,不是强制标准。实际工作中,很少有 API 能完美符合所有规范。
比如有些团队就喜欢所有接口都用 POST,URL 里带动词:
POST /api/user/getList
POST /api/user/add
POST /api/user/delete
这种写法虽然不 RESTful,但只要团队达成一致、用得舒服,也没问题。
6.2 PUT和PATCH有什么区别?
| 方法 | 语义 | 示例 |
|---|---|---|
PUT |
完整替换资源 | 需要传用户的所有字段 |
PATCH |
部分更新资源 | 只传需要修改的字段 |
python
# PUT - 需要完整数据
PUT /users/1
Body: {"name": "张三", "email": "new@example.com", "age": 26}
# PATCH - 只传要改的
PATCH /users/1
Body: {"name": "张三丰"}
6.3 返回的状态码怎么选?
| 场景 | 状态码 |
|---|---|
| 查询成功 | 200 OK |
| 创建成功 | 201 Created |
| 删除成功 | 204 No Content |
| 参数错误 | 400 Bad Request |
| 未登录 | 401 Unauthorized |
| 无权限 | 403 Forbidden |
| 资源不存在 | 404 Not Found |
| 服务器错误 | 500 Internal Server Error |
6.4 URI应该用单数还是复数?
推荐用复数:
✅ /users 用户资源集合
✅ /users/1 用户集合中 ID 为 1 的资源
❌ /user/1 不推荐
但如果资源确实只有一个(比如当前登录用户),可以用单数:
GET /user/me 当前登录用户
GET /settings 当前用户的设置(只有一份)
七、总结
本文介绍了 RESTful API 的核心概念和 FastAPI 实现,重点包括:
- REST的含义 :表现层状态转移,一种
API设计风格 - 核心原则 :
URI用名词、HTTP方法表示动作、无状态、统一响应格式 - FastAPI实现 :用
Pydantic定义模型,用装饰器定义路由 - 实战技巧 :嵌套资源、非
CRUD操作、统一错误处理
RESTful设计规范速查表:
| 操作 | HTTP方法 | URI示例 | 状态码 |
|---|---|---|---|
| 查询列表 | GET |
/users |
200 |
| 查询单个 | GET |
/users/{id} |
200 / 404 |
| 创建 | POST |
/users |
201 |
| 完整更新 | PUT |
/users/{id} |
200 / 404 |
| 部分更新 | PATCH |
/users/{id} |
200 / 404 |
| 删除 | DELETE |
/users/{id} |
204 / 404 |
记住,RESTful 不是银弹,但掌握它能让你的接口更规范、更易维护。团队里大家都用同一套规范,前后端协作才能更顺畅。
热门专栏推荐
- Agent小册
- 服务器部署
- Java基础合集
- Python基础合集
- Go基础合集
- 大数据合集
- 前端小册
- 数据库合集
- Redis 合集
- Spring 全家桶
- 微服务全家桶
- 数据结构与算法合集
- 设计模式小册
- 消息队列合集
等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟