前言

在上一篇博客中,我们完成了FastAPI的环境搭建,并写出了第一个Hello World程序。今天,我们将深入学习FastAPI的核心功能,这些功能是你开发实际项目时必须掌握的内容。

本文会尽量避免复杂的代码堆砌,用最清晰的方式讲解每一个概念。请放心,即使你没有任何Web开发经验,也能轻松跟上。


一、路由系统:告别单文件噩梦

什么是路由?

简单来说,路由就是“网址”和“处理函数”之间的对应关系。

举个例子:

  • 当用户访问 http://localhost:8000/users/123 时,FastAPI需要知道应该调用哪个函数来处理这个请求

  • 当用户访问 http://localhost:8000/products/456 时,又应该调用另一个函数

这个映射关系,就是路由。

为什么需要路由模块化?

很多初学者会把所有路由写在一个main.py文件里。这在只有两三个接口时没问题,但一旦项目变大,这个文件会变得非常臃肿,难以维护。

想象一下,一个电商系统可能有:用户模块、商品模块、订单模块、购物车模块、支付模块...如果全部塞进一个文件,代码可能达到几千行。无论是找代码还是改代码,都会非常痛苦。

模块化的好处:

  • 每个模块独立一个文件,职责清晰

  • 多人协作时不会产生代码冲突

  • 方便复用和测试

  • 便于实现API版本管理

如何创建路由模块?

在FastAPI中,我们使用APIRouter来创建独立的路由模块。

首先,创建一个routers文件夹,然后在里面新建users.py文件:

# routers/users.py
from fastapi import APIRouter

# 创建路由器实例
# prefix:所有路由的统一前缀,避免重复写"/users"
# tags:在Swagger文档中分组显示
router = APIRouter(prefix="/users", tags=["用户管理"])

@router.get("/")
def get_all_users():
    """获取所有用户"""
    return [{"id": 1, "name": "张三"}, {"id": 2, "name": "李四"}]

@router.get("/{user_id}")
def get_user(user_id: int):
    """根据ID获取单个用户"""
    return {"id": user_id, "name": f"用户{user_id}"}

@router.post("/")
def create_user(name: str, age: int):
    """创建新用户"""
    return {"message": f"用户{name}创建成功", "age": age}

同样的方式,我们可以创建商品模块:

# routers/products.py
from fastapi import APIRouter

router = APIRouter(prefix="/products", tags=["商品管理"])

@router.get("/")
def get_all_products():
    return [{"id": 1, "name": "手机"}, {"id": 2, "name": "电脑"}]

@router.get("/{product_id}")
def get_product(product_id: int):
    return {"id": product_id, "name": f"商品{product_id}"}

如何在主文件中注册路由?

创建好各个模块后,我们需要在main.py中把它们注册到FastAPI应用上:

# main.py
from fastapi import FastAPI
from routers import users, products

# 创建FastAPI应用
app = FastAPI(title="我的API项目")

# 注册路由模块
app.include_router(users.router)
app.include_router(products.router)

@app.get("/")
def root():
    return {"message": "欢迎访问我的API"}

代码非常简洁。现在,我们的项目结构清晰多了:

  • main.py:只负责创建应用和注册路由

  • routers/users.py:所有用户相关的接口

  • routers/products.py:所有商品相关的接口

关于prefix和tags的说明

上面的代码中,你可能注意到了两个参数:

prefix(路由前缀):在路由模块中设置prefix="/users"后,模块内所有路由都会自动加上这个前缀。比如@router.get("/")实际对应的路径是/users/@router.get("/{user_id}")对应的路径是/users/{user_id}。这样做的好处是避免在每个函数上重复写/users

tags(标签):这个参数不影响功能,只影响Swagger文档的显示。设置tags=["用户管理"]后,所有用户相关的接口会在文档中归为一组,方便查看。


二、请求参数:客户端如何传递数据?

在Web开发中,客户端(如浏览器、手机App)需要向服务器传递各种数据。FastAPI提供了多种方式来接收这些数据,我们逐一来看。

1. 路径参数

路径参数是URL路径的一部分,通常用来标识某个具体资源。

什么是路径参数?

看这个URL:http://localhost:8000/users/123

其中的123就是路径参数,它代表用户的ID。这种设计非常直观,符合RESTful API的规范。

如何使用?

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}")
def get_user(user_id: int):
    # user_id会自动从URL中提取,并转换为int类型
    return {"user_id": user_id, "name": f"用户{user_id}"}

当用户访问/users/123时,user_id的值就是123

类型转换的优势:如果你声明user_id: int,FastAPI会自动将字符串"123"转换为整数123。如果用户传入了无法转换的值(如/users/abc),FastAPI会自动返回清晰的错误信息,不需要你写任何校验代码。

多个路径参数

@app.get("/users/{user_id}/orders/{order_id}")
def get_order(user_id: int, order_id: int):
    return {"user_id": user_id, "order_id": order_id}

访问/users/10/orders/99即可同时获取两个参数。

2. 查询参数

查询参数是URL中?后面的部分,通常用来传递筛选条件或配置选项。

什么是查询参数?

看这个URL:http://localhost:8000/users?page=1&size=10&keyword=张三

page=1size=10keyword=张三就是查询参数。它们的特点是:

  • ?开头

  • 多个参数用&连接

  • 格式为key=value

  • 顺序不重要

  • 可以省略(可选)

如何使用?

@app.get("/users")
def get_users(page: int = 1, size: int = 10, keyword: str = None):
    return {
        "page": page,
        "size": size,
        "keyword": keyword,
        "data": []  # 实际项目中这里会是查询结果
    }

这里的pagesizekeyword就是查询参数。注意它们是在函数参数中直接定义的,没有使用任何特殊语法。

必填和可选

  • 如果参数有默认值(如page: int = 1),它就是可选的

  • 如果没有默认值(如keyword: str),它就是必填的

访问/users?page=2&size=20&keyword=李四,FastAPI会自动提取这三个值并传入函数。

路径参数和查询参数的区别

特性 路径参数 查询参数
位置 URL路径中 URL中?后面
示例 /users/123 /users?page=1
用途 标识具体资源 筛选、排序、分页
是否必须 通常是 通常可选

3. 请求体(Request Body)

当客户端需要向服务器提交大量数据(如创建用户、发布文章)时,通常会使用请求体。请求体放在HTTP请求的Body中,不像URL有长度限制。

为什么需要请求体?

查询参数适合传递少量简单数据,但如果你要提交一个包含用户名、邮箱、密码、年龄、地址等多个字段的用户信息,放在URL里显然不合适。这时候就需要使用请求体。

如何使用Pydantic模型?

FastAPI使用Pydantic来定义和验证请求体的数据结构:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 定义请求体的数据结构
class UserCreate(BaseModel):
    name: str
    email: str
    age: int
    password: str

# 使用请求体
@app.post("/users")
def create_user(user: UserCreate):
    # FastAPI会自动:
    # 1. 读取请求体中的JSON数据
    # 2. 验证每个字段的类型是否正确
    # 3. 将数据封装成UserCreate对象
    return {
        "message": f"用户{user.name}创建成功",
        "email": user.email,
        "age": user.age
    }

Pydantic的优势

你不需要手动解析JSON、不需要写各种校验代码。如果客户端发送的数据格式不对(比如age传了字符串"十八"而不是数字),FastAPI会自动返回清晰的错误信息。

4. 表单数据

表单数据也是放在请求体中的,但格式不同于JSON。传统的HTML表单使用的是这种格式。

什么时候用表单?

当你需要处理传统的HTML表单提交,或者对接一些老旧的系统时,可能需要用到表单数据。

如何使用?

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login")
def login(username: str = Form(...), password: str = Form(...)):
    # Form(...) 表示这个参数来自表单数据,且是必填的
    return {"username": username, "message": "登录成功"}

注意两点:

  1. 需要使用Form(...)而不是直接写类型

  2. 需要安装python-multipart库:pip install python-multipart

5. 文件上传

FastAPI对文件上传提供了非常好的支持。

单文件上传

from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/upload")
def upload_file(file: UploadFile = File(...)):
    # file.filename:原始文件名
    # file.file:文件内容
    return {"filename": file.filename, "size": len(file.file.read())}

多文件上传

from typing import List

@app.post("/uploads")
def upload_files(files: List[UploadFile] = File(...)):
    return [f.filename for f in files]

参数类型总结

参数类型 使用场景 写法示例
路径参数 资源标识 @app.get("/{id}")
查询参数 筛选分页 def get(page: int = 1)
请求体 提交数据 def create(user: UserModel)
表单 传统表单 def login(user: str = Form(...))
文件 上传文件 def upload(file: UploadFile)

三、响应对象:服务器如何返回数据?

默认响应

FastAPI默认会自动将你返回的字典转换为JSON格式。这是最常用也是最简单的方式:

@app.get("/user")
def get_user():
    # 直接返回字典,自动转为JSON
    return {"id": 1, "name": "张三", "age": 25}

常见的响应类型

除了默认的JSON,FastAPI还支持返回其他格式:

1. HTML响应

当你的API需要返回一个完整的HTML页面(而不是数据)时使用:

from fastapi.responses import HTMLResponse

@app.get("/page", response_class=HTMLResponse)
def get_html():
    return "<h1>Hello FastAPI</h1><p>这是一个HTML页面</p>"

2. 重定向

当你想把用户从一个地址跳转到另一个地址时使用:

from fastapi.responses import RedirectResponse

@app.get("/old")
def redirect():
    # 访问/old会自动跳转到/docs
    return RedirectResponse("/docs")

3. 文件下载

当你的API需要提供文件下载功能时:

from fastapi.responses import FileResponse

@app.get("/download")
def download():
    # 返回服务器上的文件,浏览器会提示下载
    return FileResponse("files/report.pdf", filename="报告.pdf")

响应模型(Response Model)

什么是响应模型?

响应模型用来控制哪些数据应该返回给客户端。这在处理敏感信息时特别有用。

一个实际例子

假设你的数据库中有用户表,存储了idnameemailpassword_hash(密码哈希)。当你查询用户信息时,绝对不应该把password_hash返回给客户端。

使用响应模型可以轻松解决这个问题:

from pydantic import BaseModel

# 定义响应模型(只包含需要返回的字段)
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    # 注意:没有password_hash字段

# 数据库中的完整用户数据
class UserInDB(BaseModel):
    id: int
    name: str
    email: str
    password_hash: str  # 敏感字段

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    # 从数据库查询,得到完整数据(包含密码)
    db_user = UserInDB(id=1, name="张三", email="zhang@example.com", password_hash="abc123")
    # 返回时自动过滤掉password_hash,只返回UserResponse中定义的字段
    return db_user

使用response_model的好处是:你不需要手动删除敏感字段,FastAPI会自动处理。

状态码(Status Code)

HTTP状态码用来告诉客户端请求的结果。常见的有:

  • 200:成功(GET请求默认)

  • 201:创建成功(通常用于POST请求)

  • 204:成功但无内容(通常用于DELETE请求)

  • 400:客户端错误(如参数不对)

  • 401:未认证

  • 403:无权限

  • 404:资源不存在

  • 500:服务器内部错误

指定状态码的方式:

@app.post("/users", status_code=201)  # 创建成功返回201
def create_user():
    return {"message": "用户创建成功"}

@app.delete("/users/{user_id}", status_code=204)  # 删除成功无内容返回
def delete_user(user_id: int):
    # 删除逻辑
    return None  # 204响应不应该有body

四、中间件:请求和响应的拦截器

什么是中间件?

中间件是一个在请求到达你的路由函数之前、以及响应返回给客户端之后,执行一些代码的组件。

可以把中间件想象成一个安检通道:

  • 请求进来时,先经过安检(中间件)

  • 安检通过后,才进入业务处理(路由函数)

  • 业务处理完成后,响应出去之前,还要再经过一次安检

中间件能做什么?

中间件的应用场景非常多:

  • 记录每个请求的日志

  • 统计接口响应时间

  • 添加统一的响应头(如X-Powered-By

  • 请求限流(防止恶意请求)

  • 全局异常处理

  • CORS跨域处理

  • Gzip压缩响应内容

如何编写中间件?

下面是一个记录请求日志和响应时间的中间件:

from fastapi import FastAPI, Request
import time

app = FastAPI()

@app.middleware("http")
async def log_middleware(request: Request, call_next):
    # 这部分代码在请求进入路由前执行
    start_time = time.time()
    print(f"收到请求: {request.method} {request.url.path}")
    
    # 调用下一个中间件或最终的路由函数
    response = await call_next(request)
    
    # 这部分代码在响应返回给客户端前执行
    process_time = time.time() - start_time
    print(f"处理耗时: {process_time:.4f}秒")
    
    # 可以修改响应头
    response.headers["X-Process-Time"] = str(process_time)
    
    return response

代码解释

  1. @app.middleware("http"):声明这是一个HTTP中间件

  2. request:请求对象,可以从中获取URL、请求头、客户端IP等信息

  3. call_next:下一个中间件或路由函数

  4. await call_next(request):执行后续处理,返回响应对象

  5. 可以在call_next前后分别添加请求前和响应后的逻辑

内置中间件

FastAPI(实际是Starlette)提供了多个实用的内置中间件:

CORS中间件(最常用):

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # 允许的前端地址
    allow_credentials=True,
    allow_methods=["*"],  # 允许所有HTTP方法
    allow_headers=["*"],  # 允许所有请求头
)

为什么需要CORS?因为浏览器有安全策略,默认不允许网页向不同域名的服务器发请求。如果你的前端和后端不在同一个域名下,就需要配置CORS中间件。

GZip中间件(压缩响应):

from fastapi.middleware.gzip import GZipMiddleware

app.add_middleware(GZipMiddleware, minimum_size=1000)
# 响应大于1KB时自动压缩,可以显著减少传输流量

中间件的执行顺序

如果你添加了多个中间件,它们的执行顺序很重要:

请求 → 中间件1(前) → 中间件2(前) → 路由函数 → 中间件2(后) → 中间件1(后) → 响应

简单说:请求阶段从上往下执行,响应阶段从下往上执行。


五、综合示例:一个简单的用户管理系统

把今天学到的知识结合起来,我们写一个简单的用户管理API:

# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List

app = FastAPI(title="用户管理系统")

# 定义数据模型
class User(BaseModel):
    id: int
    name: str
    email: str
    age: int

class UserCreate(BaseModel):
    name: str
    email: str
    age: int

# 模拟数据库
fake_db = []
current_id = 1

# 获取所有用户
@app.get("/users", response_model=List[User])
def get_users():
    return fake_db

# 获取单个用户
@app.get("/users/{user_id}", response_model=User)
def get_user(user_id: int):
    for user in fake_db:
        if user.id == user_id:
            return user
    raise HTTPException(status_code=404, detail="用户不存在")

# 创建用户
@app.post("/users", status_code=201, response_model=User)
def create_user(user_data: UserCreate):
    global current_id
    new_user = User(id=current_id, **user_data.dict())
    fake_db.append(new_user)
    current_id += 1
    return new_user

# 删除用户
@app.delete("/users/{user_id}", status_code=204)
def delete_user(user_id: int):
    for i, user in enumerate(fake_db):
        if user.id == user_id:
            fake_db.pop(i)
            return
    raise HTTPException(status_code=404, detail="用户不存在")

启动服务后:

  • 访问/docs可以看到自动生成的API文档

  • 可以通过POST接口创建用户

  • 可以通过GET接口查询用户

  • 可以通过DELETE接口删除用户


六、总结

今天的内容比较多,我们回顾一下核心要点:

1. 路由模块化

  • 使用APIRouter把不同模块的接口分开

  • prefix统一路由前缀

  • tags在文档中分组

2. 请求参数

  • 路径参数:URL中的变量,如/users/{id}

  • 查询参数:?后面的键值对

  • 请求体:使用Pydantic模型,适合提交复杂数据

  • 表单和文件上传:特殊场景使用

3. 响应对象

  • 默认返回字典自动转JSON

  • 可用response_model过滤敏感字段

  • status_code指定HTTP状态码

4. 中间件

  • 在请求前后执行通用逻辑

  • 常用于日志、计时、CORS、压缩等

掌握了这些核心功能,你已经可以开发大部分Web API了。下一篇博客我们会深入讲解数据库操作和依赖注入,敬请期待!


练习题

  1. 创建一个商品管理模块,包含商品的新增、查询、删除功能

  2. 为你的API添加一个日志中间件,记录每个请求的路径和处理时间

  3. 尝试使用响应模型,确保删除用户时只返回状态码,不返回内容

有任何问题,欢迎在评论区留言讨论!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐