创建第一个 FastAPI 程序

这篇教程主要面向有一定 Python 基础、想快速了解 FastAPI 的读者,不会从零讲解所有涉及的概念。比如同步与异步、服务器、HTTP 协议等内容,我会在用到时做简单说明。如果你对某个概念感兴趣,但还不太了解,可以记下对应的关键词,再搜索相关教程深入学习。

1. 创建项目并启动服务

第一步是创建项目环境。我这里使用 uv 管理 Python 项目。首先执行 uv init 初始化项目,然后执行 uv add fastapi,将 FastAPI 添加到项目依赖中。

接下来创建 main.py,写入下面的代码:

Python
from fastapi import FastAPI
app=FastAPI()
@app.get("/")
async def root():
    return {"message":"Hello World"}

这是一个最简单的 FastAPI 程序。接下来在终端执行下面的命令,启动项目:

Shell
uv add uvicorn
uv run uvicorn main:app --reload

这里先执行了 uv add uvicorn,因为 uvicorn 也是一个需要安装的 Python 库。它是一个 ASGI Web 服务器,负责监听请求,并将请求交给 FastAPI 应用处理。ASGI 可以先简单理解为 Python Web 应用与服务器之间的一套异步接口规范,FastAPI 通过它与 Uvicorn 这样的服务器协作,暂时不需要深入了解。

下面把启动命令拆开看:

  • uv run:在当前项目的环境中运行后面的命令。
  • uvicorn:启动 Uvicorn 服务器。
  • main:app:冒号前面的 main 表示 Python 模块,也就是 main.py;冒号后面的 app 表示该模块中 FastAPI 实例的变量名。
  • --reload:开启热重载。开发过程中修改代码后,Uvicorn 会自动重启服务,不需要每次都手动停止再启动。

例如,最开始访问根路由时,页面返回的是 Hello World。如果我们把代码中的 World 删除,只保留 Hello,开启热重载后,Uvicorn 会自动重启。重新刷新网页,就能看到修改后的内容。

如果不加 --reload,代码修改后服务器不会自动重启,网页仍然可能显示旧的结果,需要我们手动停止并重新启动服务。需要注意,--reload 主要用于开发调试,正式部署时通常不会开启。

2. 解读 FastAPI 代码

再回到刚才的代码:

Python
from fastapi import FastAPI
app=FastAPI()
@app.get("/")
async def root():
    return {"message":"Hello World"}

逐行来看:

  • from fastapi import FastAPI:从 fastapi 库中导入 FastAPI 类。
  • app=FastAPI():创建一个 FastAPI 实例,并将它赋值给变量 app。后续注册路由等操作都围绕这个实例进行。
  • @app.get("/"):注册一个路由,表示当客户端使用 GET 方法访问 / 路径时,执行下面的函数。
  • async def root():定义一个异步函数 root。当请求匹配到这个路由时,FastAPI 就会调用它。
  • return {"message":"Hello World"}:返回一个 Python 字典。FastAPI 会将它处理成 JSON 响应,发送给客户端。

这里的 async 表示函数是异步函数。异步编程常用于处理网络请求、数据库访问等 I/O 操作,能够在等待某项操作完成时处理其他任务。不过,异步的优势并不是只要加上 async 就会自动出现,通常还需要配合 await 调用支持异步的操作。如果在异步函数中直接执行耗时的同步阻塞操作,仍然可能阻塞事件循环。

FastAPI 也支持普通的 def 函数,并不是所有接口都必须写成 async def。如果使用的库提供异步接口,可以考虑使用 async def 和 await;如果使用的是同步阻塞库,普通 def 往往更合适。FastAPI 会在线程池中执行普通 def 路由函数,避免这类同步操作直接阻塞事件循环。

当前这个 Hello World 示例没有执行任何需要等待的异步操作,因此把 root 写成普通的 def 也能正常运行。这里使用 async def,主要是为了展示 FastAPI 常见的异步路由写法。暂时不了解的话,可以搜索“同步与异步”“async/await”等关键词。

程序启动后,终端会显示一个访问地址。默认情况下,本地开发地址通常是:

http://127.0.0.1:8000/

其中 127.0.0.1 表示本机,8000 是服务监听的端口。直接在浏览器中访问,就能看到接口返回的内容:

根路由返回 Hello World

可以看到,我们在 Python 中返回的是字典,但浏览器展示的是 JSON。这部分转换由 FastAPI 自动处理,不需要我们手动把字典转换成 JSON 字符串。

3. 交互式接口文档

FastAPI 还有一个很方便的功能:自动生成交互式接口文档。

启动程序后,访问:

http://127.0.0.1:8000/docs

就可以打开交互式文档。默认使用的是 Swagger UI,它会根据我们注册的路由和参数信息生成接口说明。

FastAPI 交互式接口文档

点击接口,再点击 Try it out,就可以填写参数并尝试调用接口。填写完成后点击 Execute 执行请求,下方的 Response body 就会显示接口返回的内容。

在交互文档中执行接口并查看响应

这个功能在开发和调试时非常实用:接口写好之后,不需要另外编写前端页面,也可以直接在文档中测试请求和查看响应。

路由

路由可以理解为:客户端访问某个 URL 时,服务器根据请求方法和路径,找到对应的处理函数。

例如,大家平时可能见过这样的 B 站视频链接:

https://www.bilibili.com/video/BV1Pin26oEYz/

其中 www.bilibili.com 是域名,后面的 /video/BV1Pin26oEYz/ 是 URL 路径。用户访问这个地址时,B 站的服务器会根据请求路径找到对应的处理逻辑,再返回视频相关的数据或页面。

回到 FastAPI:

Python
@app.get("/")

这里的 app 是 FastAPI 实例,get 表示 HTTP 请求方法,后面的 "/" 则是请求路径。常见的请求方法还有 POST、PUT、PATCH 和 DELETE,它们通常用于不同类型的操作。例如,GET 常用于获取数据,POST 常用于提交数据或创建资源。具体使用哪种方法,要根据接口的用途决定。

路径参数与 Path 参数校验

先看一个例子:

Python
from fastapi import FastAPI
app=FastAPI()
@app.get("/book/{id}")
async def get_book(id:int):
    return {"id":id,"title":f"这是第{id}本书。"}

假设我们正在开发一个图书管理系统,希望通过接口查询不同书籍的信息。显然,不可能为每一本书都单独定义一个路由。这时就可以使用路径参数,把书籍的 id 放在 URL 路径中,再根据这个 id 查询对应的信息。

例如,访问 /book/1,就可以将 1 传递给函数中的 id 参数。

通过路径参数查询图书

可以看到,函数成功读取到了路径中的 id,并将它放进返回结果中。这里的 id:int 是 Python 的类型注解,表示 id 应该是一个整数。

FastAPI 会根据类型注解解析和校验请求参数。如果传入的内容能够转换为整数,就继续执行函数;如果传入的是字母 a 这样的内容,就无法通过整数校验,FastAPI 会返回参数校验错误,而不会正常执行接口函数。

路径参数类型校验失败的提示

使用 Path 约束参数

除了指定参数类型,FastAPI 还可以通过 Path 为路径参数添加更多校验条件和描述信息。

例如,假设图书管理系统中只有 50 本书,那么书籍 id 的取值范围就应该是 1 到 50。我们可以使用 Path 限制参数范围。

下面列出一些比较常用的参数:

参数说明
...表示参数必填
gt / ge大于 / 大于等于
lt / le小于 / 小于等于
description参数描述
min_length / max_length字符串最小长度 / 最大长度

参数不止这些,这里只列出比较常用的一部分。gt、ge、lt、le 主要用于数字约束;min_length 和 max_length 则用于字符串长度限制。

使用方式如下:

Python
from fastapi import FastAPI,Path
app=FastAPI()
@app.get("/book/{id}")
async def get_book(id:int=Path(...,ge=1,le=50,description="书籍的id,取值范围为1-50")):
    return {"id":id,"title":f"这是第{id}本书。"}

这里的 Path(...) 中,... 表示这个参数是必填的。它不是说调用 Path 时必须填写一个特殊的配置,而是通过 Python 的 Ellipsis 对象告诉 FastAPI:这个参数没有默认值,必须由请求提供。

启动程序后,可以在浏览器中尝试访问 /book/1、/book/20 等地址,这些都在允许范围内;如果传入 0 或 51,就会因为不符合约束而返回校验错误。

另外,description 主要是给接口使用者和开发者查看的说明,不会自动显示在普通接口响应中。打开 /docs,就能看到参数的必填标记、描述,以及最小值和最大值等约束信息。

Path 参数约束在文档中的展示

查询参数与 Query 参数校验

路径参数通常用于定位某个具体资源,而查询参数更常用于筛选、分页等场景,用来告诉服务器希望查询哪些内容。

例如,新闻列表可能有很多条数据,我们不一定希望一次性返回全部内容,而是希望指定从哪里开始取,以及每次返回多少条。这时就可以使用查询参数。

Python
from fastapi import FastAPI,Query
app=FastAPI()
@app.get("/news/news_list")
async def get_news_list(
    skip:int=Query(0,description="跳过的记录",lt=100),
    limit:int=Query(10,description="返回的记录数")
):
    return {"skip":skip,"limit":limit}

Query 和前面介绍的 Path 很相似,都可以为参数添加默认值、校验条件和描述信息。

它们的一个区别是:查询参数可以设置默认值。比如这里的 skip 默认值为 0,limit 默认值为 10。如果请求时没有传递这两个参数,FastAPI 就会使用默认值。

查询参数在 URL 中的写法也和路径参数不同。它们通常放在路径后面,以 ? 开始,多个参数之间使用 & 连接。

例如:

http://127.0.0.1:8000/news/news_list?skip=5&limit=10

这里表示跳过前 5 条记录,返回接下来的 10 条记录。具体如何查询数据,还需要在接口函数中编写相应的业务逻辑。

请求体参数与 Field 字段约束

路径参数和查询参数通常放在 URL 中,适合传递少量信息。如果需要创建或更新资源,往往需要提交更多数据,例如用户注册时的用户名和密码。这类数据通常放在 HTTP 请求体(Request Body)中。

以常见的 HTTP/1.1 请求报文为例,可以从三个部分来理解:

  1. 请求行:包含请求方法、URL 和 HTTP 协议版本。
  2. 请求头:包含请求的元数据信息,例如内容类型。
  3. 请求体:包含实际提交的数据,例如 JSON。

我们直接看一个用户注册的例子:

Python
from fastapi import FastAPI
from pydantic import BaseModel
app=FastAPI()
class User(BaseModel):
    username:str
    password:str
@app.post("/register")
async def register(user:User):
    return user

这里用到了 Pydantic 库中的 BaseModel。我们自己定义一个 User 类,让它继承 BaseModel,并声明 username 和 password 两个字段。

函数参数 user:User 告诉 FastAPI:这个接口需要接收一个符合 User 模型结构的请求体。客户端发送 JSON 后,FastAPI 会借助 Pydantic 解析并校验数据,再生成对应的 User 对象,供函数使用。

在交互式文档中也能看到,FastAPI 已经根据 User 模型自动生成了 JSON 请求体示例。执行接口后,函数将 user 返回,FastAPI 又会把它处理成 JSON 响应。

交互文档中的用户注册请求体

用户注册接口返回 JSON 数据

这里涉及对象和 JSON 之间的转换。通俗地说,把 JSON 数据解析成程序可以使用的对象,可以理解为反序列化;把对象转换成 JSON 数据,则可以理解为序列化。FastAPI 和 Pydantic 会帮我们处理这些工作,不需要手动完成。

使用 Field 添加字段约束

请求体中的字段也可以添加默认值、长度限制等校验条件。这里使用的是 Pydantic 提供的 Field 函数(不是从 FastAPI 导入),用来为模型字段声明默认值、校验规则和描述等元数据。它和前面介绍的 Path、Query 用途相似,但作用对象是 Pydantic 模型中的字段。

例如:

Python
from fastapi import FastAPI
from pydantic import BaseModel,Field
app=FastAPI()
class User(BaseModel):
    username:str=Field(default="张三",min_length=2,max_length=10)
    password:str=Field(min_length=6,max_length=20)
@app.post("/register")
async def register(user:User):
    return user

这里给 username 设置了默认值 "张三",并限制字符串长度为 2 到 10;password 没有设置默认值,因此仍然是必填字段,同时长度需要在 6 到 20 之间。

修改代码后重新打开 /docs,就能看到字段的默认值和约束信息。尝试提交过短或过长的用户名、密码,也会得到对应的校验错误提示。

Field 字段默认值与长度校验

响应类型

客户端访问某个路由后,FastAPI 会调用对应的函数,并将函数返回的内容作为响应发送给客户端。

默认情况下,如果接口函数直接返回字典、列表等 Python 数据,FastAPI 会先将它们转换为 JSON 兼容格式,再通过 JSON 响应发送给客户端。因此,前面虽然直接返回的是 Python 字典,浏览器收到的却是 JSON 数据。如果声明了 response_model,FastAPI 还会按照响应模型校验和处理返回数据。

如果需要返回的不是 JSON,而是 HTML、纯文本、文件或其他内容,FastAPI 也提供了不同的响应类。

响应类型用途示例
JSONResponse返回 JSON 数据return JSONResponse(content={"key": "value"})
HTMLResponse返回 HTML 内容return HTMLResponse(html_content)
PlainTextResponse返回纯文本return PlainTextResponse("text")
FileResponse返回文件return FileResponse(path)
StreamingResponse返回流式内容,例如通过生成器持续提供数据生成器或异步生成器
RedirectResponse重定向到其他 URLreturn RedirectResponse(url)

这里先重点了解 JSON、HTML 和文件响应。其他响应类型可以在实际使用时再深入学习。

JSON 响应

前面最简单的例子就是 JSON 响应。函数返回一个 Python 字典,FastAPI 默认将它处理成 JSON 数据。

Python
from fastapi import FastAPI
app=FastAPI()
@app.get("/hello")
async def hello():
    return {"message":"hello,world"}

启动程序后访问 /hello,就能看到对应的 JSON 数据。

JSON 响应示例

HTML 响应

默认情况下,FastAPI 返回 JSON。如果希望接口返回 HTML,可以指定响应类型。

一种方式是在路由装饰器中设置 response_class,适合响应类型比较固定的场景。例如,下面的接口会将返回的字符串作为 HTML 内容发送:

Python
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app=FastAPI()
@app.get("/html",response_class=HTMLResponse)
async def get_html():
    return "<h1>这是一级标题</h1>"

访问 /html,浏览器就会将这段 HTML 渲染成一个加粗、字号较大的一级标题。

HTML 响应示例

另一种方式是直接返回响应对象,例如 HTMLResponse、FileResponse 或 StreamingResponse。这种方式适合需要在函数内部决定具体响应内容或类型的场景。直接返回响应对象时,FastAPI 会直接发送这个对象,不再执行通常的自动 JSON 转换;即使声明了 response_model,也不会再自动对这个响应对象进行模型转换和字段过滤。

文件响应

如果需要返回文件,可以使用 FileResponse。

Python
from fastapi import FastAPI
from fastapi.responses import FileResponse
app=FastAPI()
@app.get("/file")
async def get_file():
    path="./111.jpg"
    return FileResponse(path)

这里准备一张名为 111.jpg 的图片,并确保程序运行时能够通过 ./111.jpg 找到它。这个相对路径是相对于启动程序时的当前工作目录而言的,并不一定是 main.py 所在的目录。通常可以在项目根目录启动程序,并把图片放在对应位置。

运行程序后访问 /file,接口就会返回这张图片。浏览器会根据响应中的文件类型决定如何处理它,例如直接展示图片。

FileResponse 返回图片文件

响应模型:response_model

前面介绍的响应类型主要决定接口以什么形式返回数据,例如 JSON、HTML 或文件。

除此之外,我们还可以进一步约束 JSON 响应中的数据结构。这时就可以使用路由装饰器中的 response_model 参数。

response_model 通常接收一个 Pydantic 模型,用来定义接口应该返回哪些字段,以及这些字段分别是什么类型。FastAPI 会根据这个模型校验和处理返回数据,同时也会将模型信息展示在交互式文档中。

例如,我们定义一个 News 模型,包含 id、title 和 content 三个字段,再通过 response_model=News 指定接口的响应结构:

Python
from fastapi import FastAPI
from pydantic import BaseModel
app=FastAPI()
class News(BaseModel):
    id:int
    title:str
    content:str
@app.get("/news/{id}",response_model=News)
async def get_news(id:int):
    return {
        "id":id,
        "title":"西游记",
        "content":"东方奇幻小说"
    }

启动程序后访问对应路由,就能看到接口正常返回了符合 News 模型的数据。

响应模型返回图书信息

那么,如果实际返回的数据和我们定义的模型不一致呢?

例如,下面的代码没有返回 content 字段:

Python
from fastapi import FastAPI
from pydantic import BaseModel
app=FastAPI()
class News(BaseModel):
    id:int
    title:str
    content:str
@app.get("/news/{id}",response_model=News)
async def get_news(id:int):
    return {
        "id":id,
        "title":"西游记",
    }

由于 News 模型要求必须包含 content,而接口实际返回的数据缺少这个字段,因此无法通过响应模型校验。

这类问题属于服务端返回数据不符合约定,通常会产生服务端响应校验错误,并向客户端返回 HTTP 500(Internal Server Error)。它与客户端传入错误参数时的请求校验错误不同:后者在 FastAPI 默认情况下通常返回 HTTP 422,问题出在客户端提交的数据;这里的问题则出在接口函数返回的数据。

响应数据缺少字段时的错误

因此,response_model 不只是为了让文档展示得更清楚,它还能帮助我们约束接口输出,避免意外返回不应该暴露的字段。

异常处理

接口运行过程中,可能会遇到各种异常情况。例如,客户端请求了一本不存在的书籍,这时就应该返回一个明确的错误信息,而不是继续返回正常数据。

HTTP 状态码用于表示请求的处理结果。简单来说:

  • 2xx 通常表示请求成功。
  • 4xx 通常表示客户端请求存在问题,例如请求的资源不存在。
  • 5xx 通常表示服务器处理请求时发生了错误。

在 FastAPI 中,可以使用 HTTPException 主动抛出 HTTP 错误。它来自 fastapi 库,常用参数包括:

  • status_code:指定返回的 HTTP 状态码。
  • detail:填写具体的错误说明。

例如,通过接口查询一本书,如果书籍 id 不存在,就返回 404 Not Found:

Python
from fastapi import FastAPI,HTTPException
app=FastAPI()
@app.get("/books/{id}")
async def get_book(id:int):
    id_list={1,2,3,4,5,6}
    if id not in id_list:
        raise HTTPException(status_code=404,detail="你查找的信息不存在")
    return {"id":id}

这里使用 raise 抛出异常。一旦执行到这行代码,当前请求就会中断正常处理流程,FastAPI 会生成对应的错误响应。

如果访问一个不存在的 id,就会收到 404 状态码,响应内容则是一个 JSON:

JSON
{
  "detail": "你查找的信息不存在"
}

在交互式文档中也可以看到对应的状态码和错误信息。

HTTPException 返回 404 错误信息

谢谢你读到这里。 ✧

聊两句

评论审核后显示

有疑问、补充,或者想交流一下,都可以写在这里。

评论区将在滚动到这里时加载。