【Python】基础语法入门(三十一)—— API 开发:用 FastAPI 构建 RESTful 接口

系列导读:本系列面向零基础学习者,从变量、循环一路讲到 Web 开发、测试、部署。本篇是第三十一篇,带你用 FastAPI 从零搭建一个符合 RESTful 规范的接口服务。

一、什么是 API

先讲一个最接地气的比喻:你在手机上点外卖。

  1. 你(前端/App)想看菜单,于是向餐厅后厨(后端)喊一声:"给我菜单!"
  2. 后厨把菜单递给你(返回 JSON 数据);
  3. 你选好菜,喊一声:"我要一份宫保鸡丁!"(提交数据);
  4. 后厨做好菜递给你(返回结果)。

这里你不需要知道后厨怎么炒菜,只需要知道"喊什么口号、会得到什么回应"。这个"喊口号---拿回应"的约定,就是 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 之所以"快",原因有两点:

  1. 基于 Starlette:底层是异步 ASGI 框架,性能接近 Node.js / Go。
  2. 基于 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 小于 150
  • min_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 的组合,进程管理更稳定。

十五、最佳实践

  1. 用类型注解:FastAPI 的强大依赖于此,所有参数、模型都标注类型。
  2. 用 Pydantic 模型:请求体、响应体都用 BaseModel 定义,别用裸 dict。
  3. 用 response_model 过滤:避免敏感字段泄露。
  4. 用依赖注入复用逻辑:认证、分页、数据库连接都抽成 Depends。
  5. 合理使用异步:I/O 密集用 async,CPU 密集用普通函数。
  6. 统一异常处理:自定义异常处理器,给前端一致的错误格式。
  7. 写测试:用 TestClient + pytest,保证接口行为正确。
  8. 生产用多进程:uvicorn --workers 或 gunicorn,别单进程裸跑。
  9. 配置走环境变量 :数据库地址、密钥等用 os.getenv 或 pydantic-settings 读取。
  10. 开启 CORS 要谨慎 :生产环境只允许已知的前端域名,别用 *。
  11. 版本化你的接口 :URL 里带版本号如 /api/v1/users,大改时可以平滑迁移到 v2,老客户端继续用 v1。
  12. 分页别忘总数 :列表接口返回 {items, total, page, size},前端好做分页条。
  13. 统一响应结构 :约定 {code, message, data} 格式,前端处理一致;或直接用 HTTP 状态码 + 纯数据,二选一别混用。
  14. 幂等性要重视: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。

相关推荐
懂点Tech9 小时前
ctfshow misc28:GIF 高度修改解题与脚本实现
开发语言·windows·python
comedate9 小时前
SMLib 的 Examples 导读:PyAPI / SM_API / SMLib 三组示例怎么读
python·cad·smlib·examples·sm_api
云和数据.ChenGuang9 小时前
Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制
java·开发语言·人工智能·python·langchain·1024程序员节
川石课堂软件测试10 小时前
学习软件测试需掌握的一些知识
数据库·python·sql·功能测试·网络协议·mysql·http
万化冥合10 小时前
PyBox屏幕键盘如何辅助演示键盘操作?
python·计算机外设
车软学堂11 小时前
从测试转开发,到底要补什么
网络·人工智能·python·功能测试·单片机·车载系统·自动化
天空依然蔚蓝11 小时前
分享一个自动检测网站在线状态的python程序
python·运维开发·1024程序员节
岁月宁静11 小时前
一个 AI 项目从本地到上线,要经过哪些步骤?全流程讲清楚
python·docker·agent
codists12 小时前
《crafting interpreters》阅读笔记:p45-p45
python