第一次写 FastAPI:从启动服务到看懂一次 HTTP 请求

摘要:不从大段 HTTP 概念讲起,而是先运行两个真实接口,再通过浏览器、curl 和 pytest 看清请求、路由、状态码与 JSON 响应之间的关系。

第一次写 FastAPI:请求经过路由并返回响应

图:浏览器发出请求,FastAPI 匹配路由并把处理结果包装成响应。

不少 FastAPI 入门资料一上来就解释 ASGI、异步模型和 OpenAPI,读者还没看到页面,已经被术语淹没。第一次接触 Web API,更有效的路径是先让一次请求真的跑通。

这篇文章只做一件事:从空目录启动一个 FastAPI 服务,写出根接口和健康检查,然后从实际响应反推 HTTP 请求经历了什么。完成后,你会得到一份可以直接修改的最小项目,而不是一段只能阅读的示例代码。

本系列说明:本系列所有操作默认在 04-fastapi-beginner/ 目录下执行。如果你是从公众号单独阅读某一篇,请先进入该目录再执行命令。

先把服务跑起来,看一眼最终效果

启动应用后,请求根路径:

curl -i http://127.0.0.1:8000/

响应中的日期、服务器信息可能不同,关键结果应该类似:

HTTP/1.1 200 OK
content-type: application/json

{"message":"Hello FastAPI"}

再打开 http://127.0.0.1:8000/docs,你会看到 FastAPI 根据代码自动生成的交互式 API 文档,其中包含 //health 两个接口。

先把 Python 环境隔离出来

确认 Python 版本

进入项目目录后执行:

cd 04-fastapi-beginner
python3 --version

本系列建议使用 Python 3.11 或更高版本。预期输出类似:

Python 3.12.10

具体补丁版本不需要完全相同。若系统提示找不到 python3,请先从 https://www.python.org/downloads/ 安装 Python。

创建虚拟环境

python3 -m venv .venv
source .venv/bin/activate

Windows PowerShell 使用:

python -m venv .venv
.venv\Scripts\Activate.ps1

激活成功后,终端提示符通常会出现 (.venv)。继续确认当前解释器:

python -c "import sys; print(sys.executable)"

预期路径中包含 04-fastapi-beginner/.venv。虚拟环境的作用是把本系列依赖与系统 Python、其他项目依赖隔离开。

安装依赖

python -m pip install --upgrade pip
python -m pip install -r requirements.txt

验证安装结果:

python -c "import fastapi; print(fastapi.__version__)"

预期输出:

0.139.2

项目固定关键依赖版本,是为了避免同一份代码在不同时间安装后表现不同。以后升级版本时,应先运行全部测试。

一个 FastAPI 应用其实只需要十几行

本文代码位于 examples/ch01_first_api/main.py。先完整阅读,再自己重新敲一遍:

from fastapi import FastAPI


app = FastAPI(
    title="任务管理 API",
    description="FastAPI 入门教程的第一个可运行应用",
    version="0.1.0",
)


@app.get("/")
def read_root() -> dict[str, str]:
    return {"message": "Hello FastAPI"}


@app.get("/health")
def read_health() -> dict[str, str]:
    return {"status": "ok"}

创建应用对象

app = FastAPI(
    title="任务管理 API",
    description="FastAPI 入门教程的第一个可运行应用",
    version="0.1.0",
)

app 是整个 API 应用的入口。标题、描述和版本会进入 OpenAPI 文档,并显示在 Swagger UI 页面。

注册根接口

@app.get("/")
def read_root() -> dict[str, str]:
    return {"message": "Hello FastAPI"}

这里有三个关键部分:

  • get 表示只处理 HTTP GET 请求。
  • / 表示 URL 的根路径。
  • read_root 是请求匹配后由 FastAPI 调用的 Python 函数。

函数返回 Python 字典,FastAPI 会把它转换为 JSON 响应。

注册健康检查

@app.get("/health")
def read_health() -> dict[str, str]:
    return {"status": "ok"}

健康检查接口用于回答"服务现在能否接收请求"。本文只返回固定结果;后续接入数据库后,它还可以检查关键依赖是否可用。

浏览器能打开还不够,再用 curl 看 HTTP

启动开发服务器

确认终端位于 04-fastapi-beginner,并且虚拟环境已经激活:

fastapi dev examples/ch01_first_api/main.py

看到服务器启动并监听 http://127.0.0.1:8000 即表示成功。这个命令会监视代码文件,保存修改后自动重启,适合本地开发。

当前终端需要保持运行。另开一个终端,进入相同目录并激活虚拟环境,再继续下面的验证。

使用浏览器验证

依次打开:

前两个地址应分别显示:

{"message":"Hello FastAPI"}
{"status":"ok"}

/docs 页面展开 GET /health,点击 Try it out,再点击 Execute。确认响应状态码为 200

使用 curl 验证

浏览器隐藏了很多 HTTP 细节。增加 -i 可以同时查看响应头和响应体:

curl -i http://127.0.0.1:8000/health

重点观察:

  • 第一行状态码是不是 200 OK
  • content-type 是否包含 application/json
  • 最后一行是否为 {"status":"ok"}

再故意访问不存在的地址:

curl -i http://127.0.0.1:8000/not-found

预期状态码是 404 Not Found,响应体类似:

{"detail":"Not Found"}

这说明请求确实到达了服务,只是没有找到匹配的路径。

使用自动化测试验证

停止开发服务器不是必须的。直接在另一个已激活虚拟环境的终端执行:

python -m pytest tests/test_ch01.py -q

预期结果:

...                                                                      [100%]
3 passed

测试不仅检查两个接口,还检查生成的 OpenAPI 文档是否包含这两个路径。以后修改代码时,只需再次运行命令,就能知道原有行为有没有被意外破坏。

浏览器、命令行和自动化测试共同验证接口

图:同一个接口要从浏览器结果、HTTP 细节和自动化契约三个角度验证。

一次 GET 请求究竟走了哪些步骤

一次请求发生了什么

访问 http://127.0.0.1:8000/health 时,可以把过程简化为:

浏览器或 curl
  -> 发送 GET /health
  -> FastAPI 查找匹配的路由
  -> 调用 read_health()
  -> 把 Python 字典转换成 JSON
  -> 返回 200 OK

现在先记住四个词:

名称本文示例含义
HTTP 方法GET想对资源做什么
路径/health想访问哪个接口
状态码200服务器处理结果
响应体{"status":"ok"}服务器返回的数据

为什么返回字典却收到 JSON

Python 函数返回的是:

{"status": "ok"}

网络响应体是:

{"status":"ok"}

FastAPI 负责在二者之间转换,并把响应类型设置为 application/json。客户端不需要理解 Python 字典,只需要理解通用的 JSON 格式。

装饰器做了什么

@app.get("/health") 把"GET 方法 + /health 路径"和下面的函数关联起来。如果删除装饰器,函数仍然是普通 Python 函数,但它不再是可以通过网络访问的接口。

为什么这里使用 def

FastAPI 同时支持普通 defasync def。本文函数没有等待数据库、外部 HTTP 请求等异步操作,因此先使用更熟悉的普通函数。后续遇到真实 I/O 场景时再专门比较二者;不要为了"看起来异步"而机械地给所有函数加 async

/docs 从哪里来

FastAPI 会根据路由、类型注解和应用元数据生成 OpenAPI 文档,再由 Swagger UI 展示。本文只写了两个路由,但已经同时得到机器可读的 /openapi.json 和可交互的 /docs

一次请求经过路由匹配后返回结构化响应

图:客户端发出请求,FastAPI 匹配路由并把返回值组织成结构化响应。

第一次启动最常见的五个阻塞

ModuleNotFoundError: No module named 'fastapi'

常见原因是虚拟环境没有激活,或依赖被安装到了另一个 Python。依次执行:

python -c "import sys; print(sys.executable)"
python -m pip show fastapi

第一条路径应位于 .venv,第二条应能显示 FastAPI 信息。否则重新激活环境并安装依赖。

fastapi: command not found

先确认虚拟环境已激活,再执行:

python -m pip install -r requirements.txt

也可以检查命令位置:

which fastapi

Windows PowerShell 使用 Get-Command fastapi

Address already in use

默认的 8000 端口已经被其他程序占用。开发时可以临时换端口:

fastapi dev examples/ch01_first_api/main.py --port 8001

此时访问地址也要改成 http://127.0.0.1:8001

修改代码后结果没有变化

检查保存的是否为 examples/ch01_first_api/main.py,观察运行服务器的终端有没有重新加载日志。如果通过其他命令启动,先按 Ctrl+C 停止,再使用本文命令重新启动。

/docs 能打开,但自定义地址返回 404

对照 /docs 中显示的路径,检查斜杠和拼写。例如 /health/heath 是两个不同的路径,/ 也不能遗漏。

跑通之后,顺手改出自己的第一个接口

先改一个小地方:修改欢迎语

把根接口响应改成:

{"message":"我的任务 API 已启动"}

保存文件后刷新浏览器,确认开发服务器自动加载了修改。注意:现有测试会失败,你也需要同步修改 test_read_root 的预期值,再让测试恢复通过。

再加一个接口:增加关于接口

新增 GET /about,返回:

{
  "name": "任务管理 API",
  "version": "0.1.0"
}

完成后进行三项检查:

  1. 浏览器能访问 /about
  2. /docs 中出现 /about
  3. 为它新增一个 pytest 测试。

故意制造一次错误:制造 404

把健康检查装饰器中的路径临时改为 /healthz,但仍然访问 /health。回答:

  • 返回了什么状态码?
  • 服务器是否已经崩溃?
  • /docs 对定位问题有什么帮助?

完成观察后把路径恢复。

三个值得亲手验证的细节

  1. 为什么接口返回 JSON,而不是直接返回 Python 字典的字符串?
  2. 200404 分别说明什么?
  3. 如果只修改函数名 read_health,请求地址会不会变化?请先预测,再实验。

最后,用测试固定这次改动

浏览器和 curl 适合观察接口,自动化测试负责在后续修改中守住这些行为。执行:

python -m pytest tests/test_ch01.py -q
python tests/validate_course.py

预期看到 3 passed,以及:

OK: all 16 articles satisfy the shareable tutorial contract

如果你完成了 /about 扩展并增加了测试,测试数量会大于 3,这是正常的。

写在最后

你已经完成了一个最小但真实的 HTTP API 闭环:客户端发出带方法和路径的请求,FastAPI 找到对应函数,把函数返回的 Python 数据转换成 JSON,并通过状态码说明处理结果。

下一篇将让路径变得可变化:用 /tasks/{task_id} 查询指定任务,并使用查询参数完成筛选和分页。届时我们也会主动输入错误参数,观察 FastAPI 如何利用 Python 类型注解保护接口。