系列导读:本系列面向零基础学习者,从变量、循环一路讲到 Web 开发、测试、部署。本篇是第三十一篇,带你用 FastAPI 从零搭建一个符合 RESTful 规范的接口服务。
一、什么是 API
先讲一个最接地气的比喻:你在手机上点外卖。
- 你(前端/App)想看菜单,于是向餐厅后厨(后端)喊一声:"给我菜单!"
- 后厨把菜单递给你(返回 JSON 数据);
- 你选好菜,喊一声:"我要一份宫保鸡丁!"(提交数据);
- 后厨做好菜递给你(返回结果)。
这里你不需要知道后厨怎么炒菜,只需要知道"喊什么口号、会得到什么回应"。这个"喊口号---拿回应"的约定,就是 API(Application Programming Interface,应用程序编程接口)。
在 Web 世界里,API 通常指:后端暴露出一组 URL,前端通过 HTTP 协议访问这些 URL,拿到 JSON 格式的数据。而 RESTful 则是一套"怎么设计这些 URL 才优雅"的风格约定。
二、RESTful 设计原则
REST(Representational State Transfer)由 Roy Fielding 在 2000 年的博士论文中提出。它的核心思想是:把一切抽象为"资源",用 HTTP 方法表示对资源的操作。
2.1 HTTP 方法语义表
| HTTP 方法 | 语义 | 幂等? | 安全? | 典型场景 |
|---|---|---|---|---|
| GET | 获取资源 | 是 | 是 | 查看用户列表 |
| POST | 新建资源 | 否 | 否 | 创建一个待办 |
| PUT | 整体替换资源 | 是 | 否 | 更新用户全部信息 |
| PATCH | 局部更新资源 | 是 | 否 | 只改用户昵称 |
| DELETE | 删除资源 | 是 | 否 | 删除一条待办 |
幂等:多次执行结果相同;安全:不改变服务器状态。
2.2 URL 设计
好的 URL 应该像"名词",而不是"动词":
# ❌ 不推荐:URL 里带动词
POST /createUser
GET /getUserById?id=1
POST /deleteTodo
# ✅ 推荐:URL 是资源名词,动作交给 HTTP 方法
POST /users # 创建用户
GET /users/1 # 获取 id=1 的用户
DELETE /todos/42 # 删除 id=42 的待办
2.3 常见状态码
| 码段 | 含义 | 常见 |
|---|---|---|
| 2xx | 成功 | 200 OK、201 Created、204 No Content |
| 3xx | 重定向 | 301 Moved |
| 4xx | 客户端错误 | 400 Bad Request、401 Unauthorized、404 Not Found、422 Unprocessable Entity |
| 5xx | 服务端错误 | 500 Internal Server Error、503 Service Unavailable |
2.4 资源命名规范
- 用复数名词:
/users而非/user - 用小写 + 连字符:
/user-profiles而非/userProfiles - 嵌套表达从属:
/users/1/todos表示"用户1的待办"
三、FastAPI 简介
Python 里做 Web API 的框架不少,最常见的三个:
| 框架 | 特点 | 适用 |
|---|---|---|
| Flask | 轻量、灵活、生态成熟 | 小到中型项目、快速原型 |
| Django | 大而全、自带 ORM/Admin | 内容管理、企业级 |
| FastAPI | 现代、异步、自动文档、类型校验 | API 服务、微服务 |
FastAPI 之所以"快",原因有两点:
- 基于 Starlette:底层是异步 ASGI 框架,性能接近 Node.js / Go。
- 基于 Pydantic:用 Python 类型注解自动做请求/响应校验,并生成 OpenAPI 文档。
据官方基准测试,FastAPI 的吞吐量可达 Flask 的 6~8 倍。
3.1 三者更细致的对比
如果你还在纠结选哪个,可以参考下面的决策思路:
- 做纯 API 服务、微服务、对接前端/移动端:首选 FastAPI。它天生为 API 而生,自动文档省去大量手写文档的时间,类型校验让接口契约清晰可执行。
- 做传统网站、需要服务端渲染模板、生态插件多:选 Flask。Flask 上手快、教程多、插件丰富,但接口校验和文档要自己搞或装额外库。
- 做内容管理系统、后台管理、需要自带 ORM 和 Admin 界面:选 Django。Django"全家桶"开箱即用,但体量大、学习曲线陡,做轻量 API 有杀鸡用牛刀之嫌。
- 既要 Django 的完整又要 FastAPI 的异步:可以看 Django REST Framework(DRF),但异步支持不如 FastAPI 原生。
3.2 FastAPI 的适用场景举例
- 秒杀/抢购系统的库存查询接口(高并发、低延迟)
- 移动端 App 的后端数据接口(需要清晰文档给前端联调)
- 内部微服务的某个节点(异步 I/O、轻量部署)
- AI 模型推理的对外服务接口(把模型包装成 HTTP 接口)
- 物联网设备上报数据的接收服务(大量短连接、异步处理)
可以看到,凡是"接口密集、并发高、要文档"的场景,FastAPI 都很合适。
四、安装与第一个 API
4.1 安装
pip install fastapi uvicorn
fastapi:框架本体uvicorn:ASGI 服务器,用来运行 FastAPI 应用
4.2 Hello API
新建 main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"message": "Hello, FastAPI!"}
4.3 运行
uvicorn main:app --reload
main:app表示main.py文件里的app对象--reload开发时热重载,改代码自动重启
运行后控制台会输出:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Application startup complete.
浏览器访问 http://127.0.0.1:8000/,看到:
{"message": "Hello, FastAPI!"}
4.4 自动文档
FastAPI 最大的爽点之一:自动生成可交互文档。
http://127.0.0.1:8000/docs------ Swagger UI,可以直接点按钮发请求测试http://127.0.0.1:8000/redoc------ ReDoc,更适合阅读的文档界面
你一行文档都没写,它就帮你把接口文档生成好了,这就是"类型即文档"的魅力。
五、路径参数与类型校验
5.1 路径参数
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
访问 /users/3 返回 {"user_id": 3}。
注意 user_id: int 这个类型注解------FastAPI 会自动校验。访问 /users/abc 会得到:
{
"detail": [
{
"type": "int_parsing",
"loc": ["path", "user_id"],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"input": "abc"
}
]
}
5.2 查询参数
from typing import Optional
@app.get("/todos")
def list_todos(skip: int = 0, limit: int = 10, keyword: Optional[str] = None):
return {"skip": skip, "limit": limit, "keyword": keyword}
访问 /todos?skip=0&limit=20&keyword=python:
{"skip": 0, "limit": 20, "keyword": "python"}
没传的参数会用默认值,Optional[str] 表示可以传 None。
5.3 路径顺序的坑
新手常踩一个坑:路由匹配是按定义顺序的。看下面这个错误示例:
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
@app.get("/users/me")
def get_me():
return {"msg": "当前登录用户"}
访问 /users/me 时,FastAPI 会先匹配到 /users/{user_id},把 me 当成 user_id,因为类型是 int 所以报 422 错误。正确做法是把固定路径放在动态路径前面:
@app.get("/users/me") # ✅ 先定义固定路径
def get_me():
return {"msg": "当前登录用户"}
@app.get("/users/{user_id}") # 再定义动态路径
def get_user(user_id: int):
return {"user_id": user_id}
这是 RESTful 设计里一个很常见的细节,务必记住"具体在前、泛化在后"。
5.4 参数校验进阶
路径参数和查询参数也能加约束,用 Path 和 Query:
from fastapi import Path, Query
@app.get("/items/{item_id}")
def read_item(
item_id: int = Path(..., gt=0, description="必须大于0的ID"),
q: str = Query(None, min_length=2, max_length=50, description="搜索关键词")
):
return {"item_id": item_id, "q": q}
访问 /items/-1 会因为 gt=0 校验失败返回 422;q 传一个字符也会因为 min_length=2 报错。这些约束会自动体现在 /docs 文档里,前端联调时一目了然。
六、Pydantic 数据模型
Pydantic 是 FastAPI 的灵魂。它让你用类的方式定义数据结构,自动校验、自动转 JSON。
6.1 定义请求体
from pydantic import BaseModel
class TodoCreate(BaseModel):
title: str
done: bool = False
@app.post("/todos")
def create_todo(todo: TodoCreate):
return {"created": todo}
请求体(JSON):
{"title": "学FastAPI", "done": false}
响应:
{"created": {"title": "学FastAPI", "done": false}}
如果少传 title,FastAPI 自动返回 422 错误,告诉你哪个字段缺失。
6.2 字段类型与校验约束
from pydantic import BaseModel, Field
from typing import List, Optional
from pydantic import EmailStr
class User(BaseModel):
name: str = Field(..., min_length=2, max_length=20, description="用户名")
age: int = Field(..., gt=0, lt=150, description="年龄")
email: EmailStr
tags: List[str] = []
nickname: Optional[str] = None
...表示必填gt=0大于 0,lt=150小于 150min_length/max_length限制字符串长度EmailStr自动校验邮箱格式
6.3 嵌套模型
class Address(BaseModel):
city: str
street: str
class Person(BaseModel):
name: str
address: Address # 嵌套
p = Person(name="陈", address={"city": "深圳", "street": "科技路"})
print(p.address.city) # 深圳
嵌套模型让复杂的数据结构也能被自动校验。比如一个订单接口,请求体里有收货地址、商品列表、优惠券,每一层都是模型,任何一层字段不对都会被精确报错,告诉你具体是哪个嵌套字段的哪个键出了问题。这比手写一堆 if ... else ... 校验代码优雅太多,也可靠太多------校验逻辑只定义一次,不会漏。
6.4 响应模型 response_model
用 response_model 可以过滤响应字段,避免把密码等敏感字段返回出去:
class UserOut(BaseModel):
id: int
name: str
class UserInDB(UserOut):
password: str
@app.get("/users/{uid}", response_model=UserOut)
def get_user(uid: int):
return UserInDB(id=uid, name="陈", password="123456") # password 不会出现在响应里
七、完整 CRUD API 示例
下面用内存列表模拟数据库,实现待办任务的增删改查:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Todo(BaseModel):
id: int
title: str
done: bool = False
class TodoCreate(BaseModel):
title: str
done: bool = False
db: List[Todo] = []
next_id = 1
@app.get("/todos", response_model=List[Todo])
def list_todos():
return db
@app.post("/todos", response_model=Todo, status_code=201)
def create_todo(todo: TodoCreate):
global next_id
item = Todo(id=next_id, **todo.dict())
db.append(item)
next_id += 1
return item
@app.get("/todos/{tid}", response_model=Todo)
def get_todo(tid: int):
for item in db:
if item.id == tid:
return item
raise HTTPException(status_code=404, detail="Todo not found")
@app.put("/todos/{tid}", response_model=Todo)
def update_todo(tid: int, todo: TodoCreate):
for i, item in enumerate(db):
if item.id == tid:
updated = Todo(id=tid, **todo.dict())
db[i] = updated
return updated
raise HTTPException(status_code=404, detail="Todo not found")
@app.delete("/todos/{tid}", status_code=204)
def delete_todo(tid: int):
for i, item in enumerate(db):
if item.id == tid:
db.pop(i)
return
raise HTTPException(status_code=404, detail="Todo not found")
运行后,你可以用 /docs 页面逐个测试:先 POST 创建一条,再 GET 列表查看,PUT 修改,DELETE 删除。一个完整的 RESTful CRUD 就这样几十行代码搞定了。
7.1 用 curl 测试上述接口
不打开浏览器,用命令行 curl 也能测:
# 创建一条待办
curl -X POST http://127.0.0.1:8000/todos \
-H "Content-Type: application/json" \
-d '{"title":"学FastAPI","done":false}'
# 返回: {"id":1,"title":"学FastAPI","done":false}
# 查列表
curl http://127.0.0.1:8000/todos
# 返回: [{"id":1,"title":"学FastAPI","done":false}]
# 改
curl -X PUT http://127.0.0.1:8000/todos/1 \
-H "Content-Type: application/json" \
-d '{"title":"学FastAPI完成","done":true}'
# 返回: {"id":1,"title":"学FastAPI完成","done":true}
# 删
curl -X DELETE http://127.0.0.1:8000/todos/1
# 返回: (空,状态码204)
7.2 为什么用内存存储
本例用 Python 列表 db 存数据,重启服务就没了。这是为了让你先聚焦"接口怎么写",不被数据库细节干扰。真实项目会换成数据库(第十二节就接 SQLite)。但接口的形状不会变------这正是 RESTful 的好处:存储换了,URL 和方法不变,前端无感知。
八、异常处理
8.1 HTTPException
最常用的方式,直接抛出:
from fastapi import HTTPException
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id < 0:
raise HTTPException(status_code=400, detail="id 不能为负数")
return {"item_id": item_id}
访问 /items/-1 返回:
{"detail": "id 不能为负数"}
8.2 自定义异常处理器
from fastapi import Request
from fastapi.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"哎呀,{exc.name} 出问题了"},
)
@app.get("/unicorns/{name}")
def read_unicorn(name: str):
if name == "yummy":
raise UnicornException(name=name)
return {"unicorn": name}
访问 /unicorns/yummy 会返回 418 状态码和自定义消息。
自定义异常处理器的好处是:你可以把业务错误统一成前端好处理的格式,比如所有业务错误都返回 {code: 4001, message: "xxx"},而不是 FastAPI 默认的 {detail: "xxx"}。前端只要写一套错误处理逻辑就够了,不用针对不同异常写不同分支。这在多人协作、前后端分离的项目里尤其重要,统一的错误契约能减少大量沟通成本。
九、中间件
中间件是"包裹"在请求处理前后的钩子,常用于日志、计时、CORS。
9.1 计时中间件
import time
from fastapi import Request
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
response.headers["X-Process-Time"] = str(duration)
return response
每个响应都会带上 X-Process-Time 头,表示处理耗时。
9.2 CORS 中间件
前端跨域调用时必须配置 CORS:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # 允许的前端源
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
十、依赖注入 Depends
依赖注入(Dependency Injection)让你把"准备工作"抽成函数复用。
10.1 简单依赖
from fastapi import Depends
def common_params(q: Optional[str] = None, skip: int = 0, limit: int = 10):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items")
def read_items(commons: dict = Depends(common_params)):
return commons
10.2 认证依赖
from fastapi import Header
def get_token(authorization: str = Header(...)):
if authorization != "Bearer secret123":
raise HTTPException(status_code=401, detail="未授权")
return authorization
@app.get("/secure")
def secure_endpoint(token: str = Depends(get_token)):
return {"message": "你通过了认证", "token": token}
凡是加上 Depends(get_token) 的路由,都会自动要求带正确的 Authorization 头。
十一、异步路由 async def
FastAPI 支持异步,只要把 def 改成 async def,就能用 await:
import httpx
@app.get("/external")
async def fetch_external():
async with httpx.AsyncClient() as client:
resp = await client.get("https://api.github.com")
return {"status": resp.status_code}
经验法则:如果路由里有
await(比如异步 HTTP、异步数据库),用async def;如果是纯 CPU 计算或同步数据库,用普通def,FastAPI 会把它放到线程池里,避免阻塞事件循环。
十二、结合 SQLite 做持久化
把第七节的内存存储换成 SQLite:
import sqlite3
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
def get_db():
conn = sqlite3.connect("todos.db")
conn.row_factory = sqlite3.Row
return conn
class TodoCreate(BaseModel):
title: str
done: bool = False
@app.on_event("startup")
def startup():
conn = get_db()
conn.execute("""
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER DEFAULT 0
)
""")
conn.commit()
conn.close()
@app.get("/todos")
def list_todos():
conn = get_db()
rows = conn.execute("SELECT * FROM todos").fetchall()
conn.close()
return [dict(r) for r in rows]
@app.post("/todos")
def create_todo(todo: TodoCreate):
conn = get_db()
cur = conn.execute("INSERT INTO todos (title, done) VALUES (?, ?)",
(todo.title, int(todo.done)))
conn.commit()
new_id = cur.lastrowid
conn.close()
return {"id": new_id, **todo.dict()}
运行后数据会持久化到 todos.db 文件,重启服务不丢失。
十三、测试 API
FastAPI 自带 TestClient,不用真的启动服务器就能测:
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_create_and_get():
resp = client.post("/todos", json={"title": "测试", "done": False})
assert resp.status_code == 201
tid = resp.json()["id"]
resp = client.get(f"/todos/{tid}")
assert resp.status_code == 200
assert resp.json()["title"] == "测试"
用 pytest 运行即可。TestClient 底层基于 httpx,支持所有 HTTP 方法。
十四、部署
14.1 开发模式
uvicorn main:app --reload
14.2 生产模式(多进程)
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
--workers 4 启动 4 个进程,充分利用多核 CPU。
14.3 用 Gunicorn(更成熟)
pip install gunicorn
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
生产环境推荐 Gunicorn + Uvicorn Worker 的组合,进程管理更稳定。
十五、最佳实践
- 用类型注解:FastAPI 的强大依赖于此,所有参数、模型都标注类型。
- 用 Pydantic 模型:请求体、响应体都用 BaseModel 定义,别用裸 dict。
- 用 response_model 过滤:避免敏感字段泄露。
- 用依赖注入复用逻辑:认证、分页、数据库连接都抽成 Depends。
- 合理使用异步:I/O 密集用 async,CPU 密集用普通函数。
- 统一异常处理:自定义异常处理器,给前端一致的错误格式。
- 写测试:用 TestClient + pytest,保证接口行为正确。
- 生产用多进程:uvicorn --workers 或 gunicorn,别单进程裸跑。
- 配置走环境变量 :数据库地址、密钥等用
os.getenv或 pydantic-settings 读取。 - 开启 CORS 要谨慎 :生产环境只允许已知的前端域名,别用
*。 - 版本化你的接口 :URL 里带版本号如
/api/v1/users,大改时可以平滑迁移到 v2,老客户端继续用 v1。 - 分页别忘总数 :列表接口返回
{items, total, page, size},前端好做分页条。 - 统一响应结构 :约定
{code, message, data}格式,前端处理一致;或直接用 HTTP 状态码 + 纯数据,二选一别混用。 - 幂等性要重视:PUT/DELETE 应设计成幂等,重复请求不产生副作用,客户端重试才安全。
15.1 常见问题 FAQ
Q1:为什么访问接口返回 422 而不是 400? A:422 是 FastAPI 用来表示"请求体/参数校验失败"的状态码(Unprocessable Entity),比 400 更精确。400 通常指请求语法错误,422 指语法对但语义不满足校验规则。
Q2:async def 和 def 随便选吗? A:不是。如果函数里有 await(异步 I/O),必须 async def;如果是同步阻塞操作(比如同步数据库驱动、time.sleep),用普通 def,FastAPI 会自动放到线程池,避免阻塞事件循环。混用反而会降低性能。
Q3:怎么让接口返回纯文本而不是 JSON? A:用 Response:from fastapi import Response 然后 return Response("hello", media_type="text/plain")。返回 HTML 用 HTMLResponse,返回文件用 FileResponse 或 StreamingResponse。
Q4:生产环境关掉自动文档 /docs 怎么办? A:创建 app 时传参数:FastAPI(docs_url=None, redoc_url=None),访问 /docs 就会 404。