K 的一隅

Python FastAPI 后端实战

从请求到响应:FastAPI 在 Web 服务里站什么位置

HTTP 请求如何经 ASGI、Uvicorn 进入 FastAPI 路由,再变成响应与 OpenAPI 文档;框架在栈里的职责边界。

7 分钟阅读 更新于 2026-07-28
本文目录

浏览器地址栏敲下回车,到页面或 JSON 出现在屏幕上,中间经过好几层协议与进程。写后端时不必背全栈细节,但得清楚 FastAPI 管哪一段、Uvicorn 管哪一段——否则调试时会出现「路由明明写了却 404」「本地能跑线上 502」却找不到该查谁。

Web 框架在栈里的位置

可以把一次 HTTP 调用拆成三层直觉:

层级典型角色你写的代码在哪
传输与协议TCP、HTTP 解析一般不写
应用服务器Uvicorn、Hypercorn 等 ASGI 宿主配置端口、worker 数
应用框架FastAPI路由、校验、业务编排

FastAPI 不负责监听 80 端口、也不自己解析原始 HTTP 字节流;它接收 ASGI 层已经整理好的「作用域 + 消息流」,在 Python 里把路径映射到函数,再把返回值编码成响应。你关心的是:给定 method + path,哪个函数跑、返回什么结构

ASGI 与 Uvicorn:谁把请求送进来

Python 同步时代常见 WSGI(如 Gunicorn + Flask);FastAPI 基于 ASGI,天然支持 async def 与 WebSocket、SSE 等长连接场景。ASGI 应用是一个可调用对象,签名大致是 (scope, receive, send) -> Awaitable[None]——框架帮你封装成 @app.get("/items") 这种写法。

Uvicorn 是常用的 ASGI 服务器:它绑定地址、读 socket、把 HTTP 转成 ASGI 事件,再调用你的 FastAPI 实例。本地开发常见:

bash
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

app.main:app 表示从模块 app.main 导入名为 app 的 ASGI 应用对象。--reload 只应在开发用;生产环境由进程管理器拉起多个 worker,并关掉热重载。

理解分工:Uvicorn 管「进程怎么活、连接怎么接」;FastAPI 管「接到请求之后逻辑怎么走」。

生产里还常在前再加一层反向代理(Nginx、Caddy、云负载均衡):它们管 TLS 终止、限流、静态文件,再把请求转发到 Uvicorn 监听的 127.0.0.1:8000。排查 502 时要分清是代理够不到后端,还是 Uvicorn 进程挂了;排查 404 则多半在 FastAPI 路由 表没匹配上。养成「从外到内」看链路的习惯,比死记命令有用。

ASGI 与旧 WSGI 的关键差别在并发模型:WSGI 一次处理一个请求直到结束;ASGI 可在等待 I/O 时切换协程。对 CPU 占满的长计算,两种模型都不会 magically 变快——但若接口主要是等数据库、等下游 HTTP,async 路由 能少占线程。

路由:从 URL 到 Python 函数

路由 是框架最核心的映射表:HTTP 方法 + 路径模式 → 处理函数(同步或异步)。FastAPI 用装饰器或 APIRouter 注册:

python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class ItemCreate(BaseModel):
    name: str
    price: float


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


@app.post("/items", status_code=201)
async def create_item(body: ItemCreate) -> ItemCreate:
    # 此处仅演示请求体校验与响应模型,持久化留到后续篇章
    return body

几点机制:

  • 路径 /items@app.post 绑定,GET 同路径不会命中此函数。
  • body: ItemCreate 触发 Pydantic 校验:缺字段或类型不对时,框架直接返回 422,不必手写 if not name
  • 返回类型注解参与响应 schema 生成,也约束序列化形状。

路由 函数应尽量薄——只负责 HTTP 语义(状态码、Header、依赖注入入口);复杂规则放到 service 层(系列后面会展开)。

请求与响应:框架替你做了什么

一次 POST /items 进入 FastAPI 后,大致顺序是:

  1. ASGI 层把 body 流交给框架。
  2. 路由匹配命中 create_item
  3. 依赖项(若有 Depends)先解析。
  4. Pydantic 把 JSON 转成 ItemCreate 实例。
  5. 函数执行,返回值经响应模型编码为 JSON。
  6. Uvicorn 把响应写回客户端。

查询参数、路径参数、Header、Cookie 都有对应声明方式:

python
from fastapi import FastAPI, Query, Path

app = FastAPI()


@app.get("/items/{item_id}")
async def read_item(
    item_id: int = Path(..., ge=1),
    q: str | None = Query(default=None, max_length=50),
) -> dict[str, int | str | None]:
    return {"item_id": item_id, "q": q}

item_id 从路径段解析并校验 >= 1q 来自 query string,可选。类型不对时同样是 422——这是 FastAPI 相对「手写 Flask 路由 + 手动 json.loads」最省心的部分之一。

OpenAPI:文档从类型注解长出来

OpenAPI(以前叫 Swagger)描述 API 的路径、参数、请求体、响应 schema。FastAPI 的独特之处:只要你用了类型注解和 Pydantic 模型,OpenAPI 规范会在应用启动时自动生成,无需维护第二份 YAML。

启动后访问:

  • /docs —— Swagger UI 交互文档
  • /redoc —— ReDoc 风格只读文档
  • /openapi.json —— 原始 JSON schema

这对前后端协作、联调、生成客户端 SDK 都很有用。改了一个 response_model,文档跟着变,减少「代码已改文档还在说旧字段」的 drift。

可以在创建应用时定制元数据:

python
app = FastAPI(
    title="Shop API",
    version="0.1.0",
    description="示例:商品与健康检查",
)

生产环境若不想暴露 /docs,可通过 docs_url=None 关闭,或加网关层鉴权——OpenAPI 本身仍是框架能力,只是是否对外可见是部署策略。

OpenAPI 里的 components.schemas 来自 Pydantic 模型名;改类名会改 schema 标题,联调时注意客户端生成代码是否重新拉取。给 路由tagssummarydescription 参数,文档可读性会明显好于只有路径字符串:

python
@app.get("/items/{item_id}", tags=["items"], summary="按 ID 查询商品")
async def read_item_detail(item_id: int) -> dict[str, int]:
    return {"item_id": item_id}

团队可以把 /openapi.json 纳入 CI:对比本次 PR 与 main 的 diff,防止无意删除字段或改类型。契约驱动开发不必上全套工具,但「文档与代码同源」是 FastAPI 选型的主要收益之一。

同步路由与 async 路由

FastAPI 同时支持 defasync def 处理函数。async def 跑在 ASGI 事件循环上;普通 def 会在线程池里执行,避免阻塞 loop。选型不必教条:函数内部全是 async 数据库驱动,用 async def;内部调用旧式同步 ORM,可用 def 让框架帮你丢线程池。

python
@app.get("/sync-ok")
def sync_handler() -> dict[str, str]:
    return {"mode": "threadpool"}


@app.get("/async-ok")
async def async_handler() -> dict[str, str]:
    return {"mode": "coroutine"}

混用时关注监控:线程池耗尽的表现是延迟整体抬升,而不是单个 路由 报错。后续系列在数据库、依赖注入篇会回到这一分工。

从 curl 到 handler 的排查清单

本地验证最小闭环:

bash
curl -s http://127.0.0.1:8000/health
curl -s -X POST http://127.0.0.1:8000/items \
  -H 'Content-Type: application/json' \
  -d '{"name":"tea","price":12.5}'

若连接被拒绝,查 Uvicorn 是否监听、端口是否被占用。若返回 422,对照 OpenAPI 文档看请求体 schema。若返回 404,查 method 是否匹配、前缀是否被 include_router 多加一段。若 500,看 Uvicorn 日志栈 trace——通常是 路由 函数体内未捕获异常,而不是框架本身崩溃。

和「更大框架」怎么相处(不是对比表)

有人从 Django 来,习惯「电池Included」:ORM、Admin、模板一体。FastAPI 刻意保持轻:专注 HTTP API 层,数据库、任务队列、模板各选各的。有人从 Flask 来,习惯微内核自己拼;FastAPI 则把校验、文档、异步约定写进默认路径,换更少样板代码。

不必纠结「谁更好」——问:当前服务是不是以 JSON API 为主、要不要 async、团队是否愿意用类型注解换文档与校验。若是,FastAPI + Uvicorn 是合理默认;若还要内置 Admin 后台和一体化 ORM,可能 Django 更省事。本篇只建立位置感,不展开全功能对比。

选型时还可以看生态:Starlette 提供 路由 与中间件底座,Pydantic 做校验,Uvicorn 做 ASGI 宿主——四者职责清晰,换其中一层不必重写全部。学习路径上,先能在本地跑通 Uvicorn、能在 /docs 里看到 OpenAPI、能写两个 路由 返回 JSON,再往下加数据库与认证,顺序最稳。

常见误解

误解一:「FastAPI 等于 Uvicorn。」 前者是框架,后者是服务器;可以换 Hypercorn、Daphne 等 ASGI 宿主。

误解二:「写了 @app.get 就会自动注册到所有 worker。」 每个 worker 进程各自 import 应用;全局可变状态不在 worker 间共享,除非外接 Redis 等。

误解三:「OpenAPI 只是装饰品。」 它是契约;移动端、测试生成器、API 网关规则都可从 /openapi.json 派生。

误解五:「asyncio.run 和 loop 是两回事。」 谈 ASGI 宿主时,Uvicorn 持有一个长期运行的 loop;概念与 asyncio 相同,只是生命周期更长。

小结

FastAPI 站在 ASGI 应用层:Uvicorn 接连接,框架做 路由 匹配、校验与响应编码,并自动生成 OpenAPI 文档。把一次请求想成「代理 → ASGI 宿主 → 框架 路由 → 你的函数 → JSON 响应」,排错就有地图。入门时优先熟练 路由 声明与 /docs 对照,再叠加数据库与认证。下一篇从「一个 main.py 撑不住」说起,看 模块 怎么拆才方便改。