FastAPI 全栈后端(二):路由与数据模型

创作者: Yardon | GitHub: github.com/YardonYan | 版本: v1.0 |



RESTful 路由设计原则

RESTful API 的设计核心是把 URL 当作资源,HTTP 动词表示操作

复制代码
GET    /api/users          # 获取用户列表
POST   /api/users          # 创建用户
GET    /api/users/123      # 获取单个用户
PUT    /api/users/123      # 更新用户(全量)
PATCH  /api/users/123      # 更新用户(部分字段)
DELETE /api/users/123      # 删除用户

URL 用名词复数(users),动词用 HTTP method,不要在 URL 里写 getUser 之类的东西。


路径参数与查询参数

python 复制代码
from fastapi import FastAPI, Query
from enum import Enum

app = FastAPI()

class StatusEnum(str, Enum):
    active = "active"
    inactive = "inactive"

@app.get("/users/{user_id}")
async def get_user(
    user_id: int,                           # 路径参数
    include_posts: bool = False,            # 查询参数 ?include_posts=true
    status: StatusEnum = StatusEnum.active, # 枚举参数
    page: int = Query(1, ge=1),            # 页码,最小1
    size: int = Query(20, ge=1, le=100),   # 每页条数,1-100
    q: str | None = Query(None, min_length=2, description="搜索关键词"),
):
    """获取用户详情"""
    skip = (page - 1) * size
    # ... 数据库查询
    return {"user_id": user_id, "page": page, "q": q}

Query 装饰器提供了参数校验和文档描述。ge(greater or equal)、le(less or equal)确保传入值在合法范围内。

路径参数 vs 查询参数:如何选?

路径参数 (/users/123) 查询参数 (?page=2)
表示"哪个资源" 表示"怎么筛选/排序"
通常是唯一标识 可选,用于过滤/分页
必填 可选(有默认值)

请求体:Pydantic 模型进阶

python 复制代码
from pydantic import BaseModel, Field, EmailStr, field_validator
from datetime import date
from typing import Optional

class UserCreate(BaseModel):
    """创建用户时的请求体"""
    username: str = Field(..., min_length=3, max_length=50, description="用户名")
    email: EmailStr                                          # 自动验证邮箱格式
    password: str = Field(..., min_length=8)
    bio: Optional[str] = Field(None, max_length=200)
    birth_date: Optional[date] = None

    @field_validator("username")
    @classmethod
    def username_no_special_chars(cls, v: str) -> str:
        if not v.replace("_", "").replace("-", "").isalnum():
            raise ValueError("用户名只能包含字母、数字、下划线和连字符")
        return v.strip()

    @field_validator("password")
    @classmethod
    def password_strong_enough(cls, v: str) -> str:
        if not any(c.isdigit() for c in v):
            raise ValueError("密码必须包含至少一个数字")
        return v


@app.post("/users", status_code=201)
async def create_user(user: UserCreate):
    # user 已经被验证、转换完毕
    # 这里 user.username 已经被 strip 过了,email 格式已验证
    return {"id": 1234, "username": user.username}

@field_validator 可以编写复杂的自定义校验逻辑。比传统手写 if/else 清晰一百倍。


响应模型:控制输出

python 复制代码
from pydantic import BaseModel

class UserResponse(BaseModel):
    """对外输出的用户模型------绝不含 password"""
    id: int
    username: str
    email: str
    bio: str | None = None

    model_config = {"from_attributes": True}  # 允许从 ORM 对象转换

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    user = await db.query(...)
    return user  # FastAPI 自动过滤掉 password 等敏感字段

response_model 就是你的安全检查门------数据库返回了 20 个字段,但前端只能看到你声明的 5 个。


错误处理与状态码

python 复制代码
from fastapi import HTTPException

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await find_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail=f"用户 {user_id} 不存在")
    return user

# 全局异常处理器
from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
    return JSONResponse(status_code=400, content={"detail": str(exc)})

路由分组与 API 版本管理

随着应用变大,main.py 里塞满 50 个路由函数会变成噩梦。FastAPI 提供了 APIRouter 来分组:

python 复制代码
# routers/users.py
from fastapi import APIRouter

router = APIRouter(prefix="/api/v1/users", tags=["用户管理"])

@router.get("/")
async def list_users(): ...

@router.post("/")
async def create_user(): ...

@router.get("/{user_id}")
async def get_user(user_id: int): ...
python 复制代码
# main.py
from routers import users, posts, auth

app = FastAPI()
app.include_router(users.router)
app.include_router(posts.router)
app.include_router(auth.router)

文件上传与静态文件

python 复制代码
from fastapi import UploadFile, File

@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    # 不把整个文件读入内存------适合大文件
    contents = await file.read()
    with open(f"uploads/{file.filename}", "wb") as f:
        f.write(contents)
    return {"filename": file.filename, "size": len(contents)}

本章小结

概念 要点
路径参数 /users/{id} 表示资源,需精确匹配
查询参数 ?page=2 表示过滤/分页,用 Query() 校验
Pydantic 请求体验证、响应过滤、字段级校验链
APIRouter 按模块拆分路由,方便扩展

下一章:数据库与 ORM------用 SQLAlchemy 把数据持久化。


📌 创作者: Yardon | 🏠 个人网站: GlimmerAI.top

📖 本章是「FastAPI 全栈后端 」系列的第 2 章。下一章:数据库与ORM

🌟 欢迎大家来观看!

相关推荐
冬奇Lab4 小时前
Workflow 系列(03):状态管理——持久化、幂等性与版本绑定
人工智能·工作流引擎
冬奇Lab4 小时前
每日一个开源项目(第146篇):openpilot - 开源自动驾驶辅助系统,曾在 Consumer Reports 评测中超过特斯拉 Autopilot
人工智能·开源·自动驾驶
吴佳浩6 小时前
AI 工程师知识地图:模型格式、框架、部署工具一次讲明白
人工智能·aigc·ai编程
kyriewen6 小时前
我手写了一个 EventEmitter,面试官追问了 6 个问题——第 4 个我没答上来
前端·javascript·面试
IT_陈寒6 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
码农胖大海6 小时前
AI额度不够用的解决方案
人工智能
后端小肥肠7 小时前
小红书虚拟商品怎么做?我先用 Skill 跑通了壁纸品类
人工智能·aigc·agent
feiyu_gao7 小时前
从零搭建个人 AI 工作台:一个管理者的 3 个月实验
人工智能·aigc·团队管理
小林攻城狮7 小时前
使用 Transport 节流解决 Vercel AI SDK 流式渲染卡死问题
前端·react.js