FastAPI 完整使用指南 + 常用示例
FastAPI 是一个高性能、工程化的 Python Web API 框架,基于标准 Python 类型提示开发,底层依赖Starlette(ASGI 异步底层)和Pydantic(自动数据校验),原生支持自动生成交互式 API 文档,性能接近 Go/Node.js,是当前 Python 后端 RESTful API 开发的主流方案。
前置安装
需要安装 FastAPI 本体 + ASGI 运行服务器 Uvicorn:
pipinstallfastapi uvicorn一、最小入门示例
创建main.py文件:
fromfastapiimportFastAPI# 创建 FastAPI 应用实例app=FastAPI(title="示例接口",version="1.0")# 定义 GET 接口,路径为 /@app.get("/")defhello():return{"message":"Hello FastAPI"}拆解 @app.get(“/”)
- app:你创建的 FastAPI() 实例。
- .get:表示这个路由只响应 HTTP GET 方法。FastAPI 还提供 .post、.put、.delete、.patch 等方法。
- “/”:URL 路径。“/” 表示根路径,即 http://127.0.0.1:8000/。
所以 @app.get(“/”) 的意思是:当有人用 GET 方法访问根路径 / 时,执行下面这个函数来处理请求。
具体来说:
- 函数名 hello 本身不重要,可以随意取,关键是它被上面的装饰器绑定到了 GET /。
- 函数返回值会由 FastAPI 自动转换成 JSON 响应。这里返回一个字典,客户端收到的是 {“message”: “Hello World”}。
- 用 async def 定义表示这是一个协程函数,FastAPI 会在事件循环中异步执行它,适合里面有 await(比如查数据库、调外部 API)的场景。如果函数里全是同步代码,用普通的 def 也完全可以,FastAPI 会在线程池中运行它,不会阻塞事件循环。
启动服务
在终端执行命令(main是文件名,app是代码中的实例名):
uvicorn main:app--reload--reload:开发环境热重载,代码修改后自动重启,生产环境禁用。- 默认地址:
[http://127.0.0.1:8000](http://127.0.0.1:8000)
核心特性:自动生成接口文档
启动后直接访问以下地址,可在线调试接口:
- Swagger UI 交互式文档:
[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) - ReDoc 文档:
[http://127.0.0.1:8000/redoc](http://127.0.0.1:8000/redoc)
二、路径参数(Path Parameters)
把参数写在 URL 路径中,支持自动类型校验和转换。
fromfastapiimportFastAPI app=FastAPI()# 路径参数 item_id,自动约束为 int 类型@app.get("/items/{item_id}")defget_item(item_id:int):return{"item_id":item_id,"name":f"商品{item_id}"}- 装饰器声明:这个函数负责处理 GET /items/{item_id}。
- 函数签名声明:路径里的 {item_id} 会自动作为参数 item_id 传进来,并转换为 int;查询参数 ?q=xxx 对应 q。
- 函数体:拿到这些参数后做业务处理,返回值就是响应体。
简单说,装饰器负责“什么时候调用这个函数”,函数负责“拿到参数后做什么、返回什么”。
说明
- 传入
item_id=123自动识别为整数;传入非数字字符串会直接返回 422 校验错误,无需手动判断。 - 支持枚举限定参数取值范围:
fromenumimportEnumfromfastapiimportFastAPI app=FastAPI()classCategory(str,Enum):food="food"book="book"electronic="electronic"@app.get("/category/{cate}")defget_category(cate:Category):return{"category":cate.value}只能传入food/book/electronic三个值,其他值自动校验失败。
三、查询参数(Query Parameters)
URL 中?后面的参数,直接在函数中定义即可自动解析。
fromenumimportEnumfromfastapiimportFastAPI app=FastAPI()@app.get("/goods/")defget_goods(skip:int=0,limit:int=10,keyword:str|None=None):""" 商品列表接口 - skip: 起始偏移量,默认0 - limit: 每页数量,默认10 - keyword: 搜索关键词,可选参数 """return{"skip":skip,"limit":limit,"keyword":keyword}http://127.0.0.1:8000/goods/?skip=1&limit=2&keyword=3特点
- 有默认值的参数为可选参数,无默认值则为必填。
- 布尔类型自动转换:
?active=true、?active=1、?active=True都会识别为True。
四、路径参数和查询参数的本质区别
路径参数和查询参数的本质区别,可以归结为一句话:路径参数是"资源标识"的一部分,查询参数是"对资源的附加说明"。这个区别决定了它们在路由匹配、语义、设计上的不同行为。
从 HTTP 和 URL 的角度看
一个 URL 的结构是:
http://host/path?query#fragment ↑ ↑ 路径 查询字符串- 路径(path)在
?之前,用来定位服务器上的某个资源。/items/1和/items/2在 HTTP 看来是两个完全不同的 URL。 - 查询字符串(query string)在
?之后,用来给这个资源附加参数。/items?id=1和/items?id=2的路径相同,只是查询条件不同。
所以本质区别首先是:参数是否参与了"资源定位"。路径参数改变了资源地址本身,查询参数没有。
从路由匹配的角度看
FastAPI(以及大多数 Web 框架)的路由匹配只看路径部分,不看查询字符串。
@app.get("/items/{item_id}")# 匹配 /items/1、/items/2、/items/abcasyncdefread_item(item_id:int):...@app.get("/items/")# 匹配 /items/、/items/?id=1、/items/?id=2asyncdefread_items(id:int):...- 路径参数是路由模板的一部分,框架用它来区分不同路由。
/items/{item_id}和/users/{user_id}能匹配到不同函数。 - 查询参数不参与路由匹配。
/items/?id=1和/items/?name=book命中同一个函数,函数再决定读取哪些查询参数。
这是最实际的区别:路径参数决定了"调用哪个函数",查询参数只是"传给这个函数的输入"。
从语义设计的角度看
这来自 REST 的资源模型:
- 路径参数标识资源。
/items/1表示"id 为 1 的那个 item",它是一个唯一资源。删掉它、修改它,操作对象是明确的。 - 查询参数描述资源的视图。
/items?category=book&page=2表示"items 集合中,category 为 book 的第 2 页"。它不改变"items 集合"这个资源本身,只是筛选、排序、分页。
一个常用的判断方法:如果去掉这个参数,URL 还指向同一个东西吗?
/items/1去掉1变成/items/,指向的是集合,不是同一个资源 → 应该用路径参数。/items?page=2去掉page=2变成/items,还是同一个集合,只是回到第一页 → 应该用查询参数。
从代码行为的角度看
| 维度 | 路径参数 | 查询参数 |
|---|---|---|
| 在 URL 中的位置 | ?之前 | ?之后 |
| 是否影响路由匹配 | 是 | 否 |
| 是否必填 | 通常必填(否则路由不匹配) | 可有默认值,可选 |
| 函数中如何声明 | 参数名与{name}一致 | 参数名不出现在路径里 |
| 典型用途 | 定位单个资源 | 过滤、排序、分页、搜索 |
一个容易混淆的点
有人会问:/items/1和/items?id=1结果可能一样,为什么不能随便用?
因为它们的语义和扩展性不同:
/items/1是"这个资源"。如果你想同时支持/items/1/reviews(某个 item 的评论),路径参数天然支持层级嵌套。/items?id=1是"从集合里筛出 id=1"。这种写法下,/items/reviews就没法自然表达"某个 item 的评论"了。
所以本质区别不只是技术实现,更是URL 设计模型:路径表达"是什么资源",查询表达"要什么样的资源"。
总结
路径参数是资源地址的组成部分,决定路由和资源身份;查询参数是附加在地址后的选项,用来筛选或配置这个资源。前者改变"你访问的是谁",后者改变"你希望怎么看它"。
五、请求体:结合 Pydantic 自动校验
基本用法
FastAPI 的 POST 请求主要用于创建资源或提交数据,数据通常放在请求体(request body)中。
POST/PUT 请求的 JSON 体,配合 Pydantic 的BaseModel实现自动解析 + 数据校验。
# 定义请求体模型fromfastapiimportFastAPIfrompydanticimportBaseModel app=FastAPI()classItem(BaseModel):name:strprice:floattax:float|None=None@app.post("/items/")asyncdefcreate_item(item:Item):return{"name":item.name,"price":item.price,"tax":item.tax}客户端发送
POST/items/Content-Type:application/json{"name":"book","price":12.5,"tax":1.2}说明
- @app.post(“/items/”) 把这个函数绑定到 POST /items/。
- 参数 item: Item 声明为 Pydantic 模型,FastAPI 会自动把请求体 JSON 解析并校验成 Item 对象。
- 返回值自动序列化为 JSON 响应。
- FastAPI 自动从请求中读取 JSON,转换为
Item对象,并执行校验。 - 校验不通过时自动返回 422 错误和详细的错误信息,无需手动写 if 判断。
请求体参数与路径/查询参数混合
POST 路由里可以同时有路径参数、查询参数和请求体:
@app.post("/items/{item_id}")asyncdefupdate_item(item_id:int,# 路径参数q:str|None=None,# 查询参数item:Item=None,# 请求体):return{"item_id":item_id,"q":q,"item":item}请求 POST /items/5?q=test,body 为 JSON 时:
- item_id 从路径取,q 从查询串取,item 从 body 取。
- FastAPI 根据参数类型和位置自动判断来源:Pydantic 模型 → body;简单类型且名字在路径里 → 路径;其余简单类型 → 查询。
请求体的多种形式
单个模型:最常见,直接声明一个 Pydantic 模型参数。
多个模型:如果函数里有多个 Pydantic 模型参数,FastAPI 会把它们合并成一个 JSON 对象,每个模型作为顶层的一个键:
classUser(BaseModel):username:strfull_name:str|None=None@app.post("/users/")asyncdefcreate_user(user:User,item:Item):return{"user":user,"item":item}期望的 body 是:
{ "user": {"username": "alice"}, "item": {"name": "book", "price": 12.5} }用 Body 显式声明单值:如果只想接收一个简单值作为 body(而不是查询参数),用 Body:
fromfastapiimportBody@app.post("/items/")asyncdefcreate_item(name:str=Body(...),price:float=Body(...)):return{"name":name,"price":price}此时 body 是 {“name”: “book”, “price”: 12.5},而不是查询串。
嵌入单个模型:默认单个模型参数期望 body 就是模型本身;加 Body(embed=True) 可以要求它嵌在一个键下:
python
@app.post("/items/") async def create_item(item: Item = Body(embed=True)): ...期望 body 变成 {“item”: {“name”: “book”, “price”: 12.5}}。
数据校验
Pydantic 模型负责校验,字段缺失、类型不符都会返回 422 Unprocessable Entity,并附带详细错误信息:
{ "detail": [ { "type": "missing", "loc": ["body", "price"], "msg": "Field required" } ] }可以在模型里用 Field 加约束:
frompydanticimportBaseModel,FieldclassItem(BaseModel):name:str=Field(min_length=1,max_length=100)price:float=Field(gt=0)状态码与响应模型
POST 创建资源通常返回 201 Created:
fromfastapiimportstatus@app.post("/items/",status_code=status.HTTP_201_CREATED)asyncdefcreate_item(item:Item):returnitem用 response_model 控制响应体的字段(比如隐藏敏感字段):
classItemOut(BaseModel):name:strprice:float@app.post("/items/",response_model=ItemOut)asyncdefcreate_item(item:Item):returnitem表单与文件上传
如果前端提交的是表单而不是 JSON,用 Form:
fromfastapiimportForm@app.post("/login/")asyncdeflogin(username:str=Form(...),password:str=Form(...)):return{"username":username}上传文件用 UploadFile:
fromfastapiimportUploadFile,File@app.post("/upload/")asyncdefupload(file:UploadFile=File(...)):content=awaitfile.read()return{"filename":file.filename,"size":len(content)}注意:表单和文件需要 python-multipart,安装 fastapi[standard] 时已包含。
六、参数精细化校验:Path / Query
针对路径参数和查询参数,使用Path和Query实现更精细的校验规则(和 Pydantic 的Field用法一致)。
fromfastapiimportFastAPI,Path,Query app=FastAPI()@app.get("/items/{item_id}")defread_item(# 路径参数校验:必须大于0item_id:int=Path(gt=0,description="商品ID,必须大于0"),# 查询参数校验:字符串长度1-50,可选q:str|None=Query(default=None,min_length=1,max_length=50)):return{"item_id":item_id,"q":q}补充:请求头 / Cookie 参数
fromfastapiimportHeader,Cookie@app.get("/header/")defget_header(user_agent:str|None=Header(default=None),# 获取请求头 User-Agentsession_id:str|None=Cookie(default=None)# 获取 Cookie 中的 session_id):return{"user_agent":user_agent,"session_id":session_id}七、响应模型:统一格式 + 字段过滤
通过response_model指定返回数据的结构,自动过滤敏感字段(比如密码)、统一返回格式。
fromfastapiimportFastAPIfrompydanticimportBaseModel app=FastAPI()# 请求模型:包含密码classUserCreate(BaseModel):username:strpassword:stremail:str# 响应模型:不返回密码classUserResponse(BaseModel):username:stremail:str@app.post("/users/",response_model=UserResponse)defcreate_user(user:UserCreate):# 模拟数据库保存,返回完整用户对象(含密码)user_in_db=user.model_dump()# 自动按照 UserResponse 过滤字段,密码不会返回给前端returnuser_in_db即使函数返回了包含
password的字典,FastAPI 也会自动按照response_model裁剪字段,保证数据安全。
八、HTTP 状态码与异常处理
自定义响应状态码
通过status_code参数设置接口成功时的状态码,比如 201(创建成功)、204(无内容)。
抛出 HTTP 异常
使用HTTPException主动抛出业务异常,返回标准错误响应。
fromfastapiimportFastAPI,HTTPException app=FastAPI()@app.get("/users/{user_id}")defget_user(user_id:int):ifuser_id<=0:# 抛出 400 错误raiseHTTPException(status_code=400,detail="用户ID不合法")ifuser_id>100:# 抛出 404 错误raiseHTTPException(status_code=404,detail="用户不存在")return{"user_id":user_id,"name":"测试用户"}九、依赖注入:逻辑复用(Depends)
FastAPI 的核心特性之一,用于抽离公共逻辑(分页、鉴权、参数解析等),在多个接口中复用。
示例1:通用分页依赖
把一个可复用的函数挂到路由上,让它自动解析参数并把结果传给接口函数。
fromfastapiimportFastAPI,Depends app=FastAPI()# 定义分页依赖函数defget_pagination(skip:int=0,limit:int=10):return{"skip":skip,"limit":limit}# 接口中注入依赖,自动接收分页参数@app.get("/goods/")defget_goods(pagination:dict=Depends(get_pagination)):returnpaginationget_pagination 是一个普通函数,定义了两个查询参数的默认值:
- skip 默认 0
- limit 默认 10
它返回一个字典。这个函数本身跟路由无关,是一个可复用的"依赖"。
- Depends(get_pagination) 告诉 FastAPI:在处理这个请求前,先调用 get_pagination。
- pagination: dict = … 表示把 get_pagination 的返回值(那个字典)赋给 pagination。
- 所以 get_goods 拿到的 pagination 就是 {“skip”: …, “limit”: …}。
请求时发生什么
访问 GET /goods/?skip=5&limit=20 时:
- FastAPI 看到 Depends(get_pagination),去调用 get_pagination。
- get_pagination 的参数 skip、limit 是查询参数,FastAPI 从 URL 里取值:skip=5、limit=20。
- 函数返回 {“skip”: 5, “limit”: 20}。
- 这个字典作为 pagination 传入 get_goods。
- get_goods 直接返回它,响应就是:
{“skip”: 5, “limit”: 20}
对比:不用依赖注入的写法
@app.get("/goods/")defget_goods(skip:int=0,limit:int=10):return{"skip":skip,"limit":limit}效果完全一样,但如果十个接口都要分页,就得写十遍。用 Depends 只需写一次 get_pagination,然后每个接口写 pagination: dict = Depends(get_pagination) 即可。
Depends(get_pagination) 让 FastAPI 在调用接口前先执行 get_pagination,把它解析出的分页参数自动注入到 get_goods 里。这样分页逻辑写一次,所有接口都能复用。
示例2:用户鉴权依赖(模拟)
defget_current_user(token:str|None=Query(default=None)):ifnottoken:raiseHTTPException(status_code=401,detail="未登录")return{"user_id":1,"username":"admin"}@app.get("/user/info")defuser_info(current_user=Depends(get_current_user)):return{"当前用户":current_user}十、表单数据与文件上传
1. 表单提交(application/x-www-form-urlencoded)
fromfastapiimportFastAPI,Form app=FastAPI()@app.post("/login/")deflogin(username:str=Form(),password:str=Form()):return{"username":username,"login":True}2. 文件上传(multipart/form-data)
推荐使用UploadFile,支持大文件异步处理、文件名获取、文件大小等。
fromfastapiimportFastAPI,File,UploadFile app=FastAPI()@app.post("/upload/")defupload_file(file:UploadFile=File()):return{"文件名":file.filename,"文件类型":file.content_type,}十一、路由拆分:APIRouter 模块化
项目变大后,不能把所有接口写在一个文件里,用APIRouter按模块拆分路由。
新建routers/user.py
fromfastapiimportAPIRouter router=APIRouter(prefix="/users",tags=["用户模块"])@router.get("/{user_id}")defget_user(user_id:int):return{"user_id":user_id}@router.post("/")defcreate_user():return{"msg":"创建用户"}主文件main.py挂载路由
fromfastapiimportFastAPIfromrouters.userimportrouterasuser_router app=FastAPI()# 挂载用户路由app.include_router(user_router)十二、常用配置:跨域(CORS)
前后端分离项目必须配置跨域,使用内置中间件实现:
fromfastapiimportFastAPIfromfastapi.middleware.corsimportCORSMiddleware app=FastAPI()# 配置允许的源app.add_middleware(CORSMiddleware,allow_origins=["*"],# 生产环境替换为具体域名allow_credentials=True,allow_methods=["*"],allow_headers=["*"],)典型应用场景
- 前后端分离的后端 RESTful API 开发
- 微服务内部接口、数据服务接口
- 机器学习模型的 HTTP 服务化封装
- 配合 SQLAlchemy / Tortoise-ORM 开发数据库业务系统