FastAPI 实战入门:用 Python 最快的框架 30 分钟搭建 REST API
为什么是 FastAPI?
如果你是前端开发者,习惯了 Express、Koa 或 Hono 的开发体验,那么你在 Python 世界里要找的那个框架,就是 FastAPI。
先看一组数据对比:
| 框架 | 语言 | 请求/秒 (JSON 序列化) | 类型安全 | 自动文档 |
|---|---|---|---|---|
| Express | Node.js | ~15,000 | ❌ (需 TS) | ❌ |
| Flask | Python | ~1,200 | ❌ | ❌ |
| Django REST | Python | ~800 | ❌ | 需插件 |
| FastAPI | Python | ~9,500 | ✅ 原生 | ✅ 原生 |
FastAPI 比 Flask 快 8 倍,而且自带类型校验和交互式 API 文档。这不是营销话术——它底层用了 Starlette(ASGI 框架)+ Pydantic(数据校验),性能接近 Go 和 Node.js。
💡 GitHub 81k+ stars,是 2026 年 Python Web 框架的事实标准。如果你只学一个 Python 后端框架,就是它了。
环境准备
# 创建项目目录mkdir fastapi-demo && cd fastapi-demo
# 建议用虚拟环境(别污染全局)python -m venv .venvsource .venv/bin/activate # Windows: .venv\Scripts\activate
# 安装依赖pip install "fastapi[standard]"fastapi[standard] 会自动装上 uvicorn(ASGI 服务器)、pydantic、python-multipart 等常用依赖。一行搞定,不用像 Django 那样装一堆。
第一个 API:5 行代码
创建 main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")async def root(): return {"message": "Hello, FastAPI!"}启动:
uvicorn main:app --reload打开 http://localhost:8000,你会看到 JSON 响应。再打开 http://localhost:8000/docs——惊喜来了,自动生成的 Swagger UI 交互文档,不用写一行配置。
这就是 FastAPI 的核心哲学:约定优于配置,类型即文档。
路由与路径参数
FastAPI 的路由设计非常直觉化,如果你写过 Express,几乎零学习成本:
from fastapi import FastAPI
app = FastAPI(title="书店 API", version="1.0.0")
# 路径参数 —— 类型自动校验@app.get("/books/{book_id}")async def get_book(book_id: int): return {"book_id": book_id}
# 查询参数 —— 默认值 = 可选参数@app.get("/books")async def list_books(page: int = 1, size: int = 10, keyword: str | None = None): return {"page": page, "size": size, "keyword": keyword}注意 book_id: int 这个类型标注——它不是装饰,是真正的运行时校验。如果你请求 /books/abc,FastAPI 会自动返回 422 错误:
{ "detail": [ { "type": "int_parsing", "loc": ["path", "book_id"], "msg": "Input should be a valid integer", "input": "abc" } ]}不用写一行校验代码。这就是”为什么用 FastAPI”的第一个答案。
Pydantic 模型:数据校验的核弹
这是 FastAPI 最强大的武器。如果你用过 TypeScript 的 Zod 或 io-ts,Pydantic 就是 Python 版的运行时类型校验。
from pydantic import BaseModel, Fieldfrom datetime import datetime
class BookCreate(BaseModel): """创建图书的请求体""" title: str = Field(..., min_length=1, max_length=200, examples=["Python 编程导论"]) author: str = Field(..., min_length=1, max_length=100) price: float = Field(..., gt=0, le=9999.99, description="价格,单位:元") isbn: str = Field(..., pattern=r"^\d{13}$", description="13 位 ISBN") tags: list[str] = Field(default_factory=list, max_length=10)
class BookResponse(BookCreate): """返回的图书数据""" id: int created_at: datetime
model_config = {"from_attributes": True} # 支持 ORM 对象转换为什么要分 BookCreate 和 BookResponse?这是 FastAPI 的最佳实践——请求模型和响应模型分离。就像前端的 DTO(Data Transfer Object)一样:
- 创建时不需要
id和created_at(服务端生成) - 返回时需要完整信息
- 避免内部字段泄露(比如密码哈希)
CRUD 实战:完整的书店 API
下面是一个功能完整的内存版书店 API。生产环境换成数据库就行,逻辑完全一样:
from fastapi import FastAPI, HTTPException, Queryfrom pydantic import BaseModel, Fieldfrom datetime import datetime
app = FastAPI(title="书店 API", version="1.0.0")
# ---------- 模型 ----------
class BookCreate(BaseModel): title: str = Field(..., min_length=1, max_length=200) author: str = Field(..., min_length=1, max_length=100) price: float = Field(..., gt=0, le=9999.99) tags: list[str] = Field(default_factory=list)
class BookUpdate(BaseModel): title: str | None = None author: str | None = None price: float | None = Field(None, gt=0, le=9999.99) tags: list[str] | None = None
class BookResponse(BookCreate): id: int created_at: datetime
# ---------- 模拟数据库 ----------
db: dict[int, dict] = {}counter = 0
# ---------- CRUD 路由 ----------
@app.post("/books", response_model=BookResponse, status_code=201)async def create_book(book: BookCreate): global counter counter += 1 now = datetime.now() record = {"id": counter, **book.model_dump(), "created_at": now} db[counter] = record return record
@app.get("/books", response_model=list[BookResponse])async def list_books( page: int = Query(1, ge=1), size: int = Query(10, ge=1, le=100), author: str | None = None,): items = list(db.values()) if author: items = [b for b in items if author.lower() in b["author"].lower()] start = (page - 1) * size return items[start : start + size]
@app.get("/books/{book_id}", response_model=BookResponse)async def get_book(book_id: int): if book_id not in db: raise HTTPException(status_code=404, detail=f"图书 {book_id} 不存在") return db[book_id]
@app.put("/books/{book_id}", response_model=BookResponse)async def update_book(book_id: int, book: BookUpdate): if book_id not in db: raise HTTPException(status_code=404, detail=f"图书 {book_id} 不存在") # 只更新传入的字段(None 的跳过) update_data = book.model_dump(exclude_unset=True) db[book_id].update(update_data) return db[book_id]
@app.delete("/books/{book_id}", status_code=204)async def delete_book(book_id: int): if book_id not in db: raise HTTPException(status_code=404, detail=f"图书 {book_id} 不存在") del db[book_id]几个值得说的设计细节:
response_model—— 自动过滤响应字段,只返回模型定义的内容,防止数据泄露status_code=201—— 创建资源用 201,不是 200。RESTful 规范,别含糊exclude_unset=True—— 局部更新的关键。没传的字段不覆盖,避免PUT把所有字段重置为NoneHTTPException—— FastAPI 内置的错误抛出方式,自动转成标准 JSON 错误响应
依赖注入:FastAPI 的杀手锏
这是 FastAPI 区别于所有其他 Python 框架的核心特性。如果你用过 Angular 或 NestJS 的 DI,会觉得很亲切:
from fastapi import Depends, Header, HTTPException
# 模拟认证:从 Header 中提取 tokenasync def get_current_user(authorization: str = Header(...)): if not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="无效的认证格式") token = authorization.replace("Bearer ", "") # 实际项目这里解析 JWT if token != "secret-token-123": raise HTTPException(status_code=401, detail="认证失败") return {"user_id": 1, "username": "admin"}
# 权限检查:依赖可以嵌套async def require_admin(user: dict = Depends(get_current_user)): if user["username"] != "admin": raise HTTPException(status_code=403, detail="需要管理员权限") return user
# 使用依赖@app.delete("/books/{book_id}", status_code=204)async def delete_book(book_id: int, user: dict = Depends(require_admin)): """只有管理员才能删除图书""" if book_id not in db: raise HTTPException(status_code=404, detail=f"图书 {book_id} 不存在") del db[book_id]为什么依赖注入比中间件更好?
- 中间件是全局的,依赖注入是路由级的——精确控制
- 依赖可以有返回值(比如当前用户),中间件只能往
request上挂属性 - 依赖自动出现在 API 文档里,中间件不会
- 依赖可以组合、嵌套、复用,像乐高积木
中间件与 CORS
前端开发者最熟悉的痛点——跨域。FastAPI 一行搞定:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173", "https://your-domain.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"],)自定义中间件也很直观:
import timefrom starlette.middleware.base import BaseHTTPMiddlewarefrom starlette.requests import Request
class TimingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): start = time.perf_counter() response = await call_next(request) duration = time.perf_counter() - start response.headers["X-Process-Time"] = f"{duration:.4f}" if duration > 1.0: print(f"⚠️ 慢请求: {request.method} {request.url.path} 耗时 {duration:.2f}s") return response
app.add_middleware(TimingMiddleware)错误处理:别让用户看到 500
生产环境最怕的就是裸奔的 500 错误。FastAPI 提供了全局异常处理器:
from fastapi import Requestfrom fastapi.responses import JSONResponsefrom pydantic import ValidationError
@app.exception_handler(Exception)async def global_exception_handler(request: Request, exc: Exception): """兜底:所有未捕获的异常""" # 生产环境应该接入日志系统(Sentry、ELK 等) print(f"❌ 未处理异常: {type(exc).__name__}: {exc}") return JSONResponse( status_code=500, content={ "detail": "服务器内部错误", "type": type(exc).__name__, }, )
@app.exception_handler(404)async def not_found_handler(request: Request, exc: HTTPException): return JSONResponse( status_code=404, content={"detail": "资源不存在", "path": str(request.url.path)}, )常见坑:很多人在 FastAPI 里用 try/except 包裹每个路由。别这样——用全局异常处理器统一兜底,路由里只处理业务逻辑级别的异常(比如”用户不存在”),系统级异常交给全局处理器。
项目结构:从玩具到生产
当你的 main.py 超过 200 行,就该拆分了。推荐这种结构:
fastapi-demo/├── app/│ ├── __init__.py│ ├── main.py # 入口:创建 app、注册路由│ ├── config.py # 配置管理│ ├── dependencies.py # 公共依赖(认证、数据库连接等)│ ├── routers/│ │ ├── __init__.py│ │ ├── books.py # 图书相关路由│ │ └── users.py # 用户相关路由│ ├── models/│ │ ├── __init__.py│ │ └── book.py # Pydantic 模型│ └── services/│ ├── __init__.py│ └── book_service.py # 业务逻辑├── tests/│ ├── test_books.py│ └── conftest.py├── requirements.txt└── Dockerfile路由拆分用 APIRouter:
from fastapi import APIRouter
router = APIRouter(prefix="/books", tags=["图书管理"])
@router.get("/")async def list_books(): ...
# app/main.pyfrom app.routers import books, users
app = FastAPI()app.include_router(books.router)app.include_router(users.router)和 Express 的 Router 几乎一模一样,前端同学上手零成本。
配置管理:别硬编码
from pydantic_settings import BaseSettings
class Settings(BaseSettings): app_name: str = "书店 API" debug: bool = False database_url: str = "sqlite:///./app.db" secret_key: str = "change-me-in-production" allowed_origins: list[str] = ["http://localhost:5173"]
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
settings = Settings()Pydantic Settings 会自动从环境变量和 .env 文件读取配置,类型自动转换。比如 DEBUG=true 会被解析为 Python 的 True。这比 os.getenv() 然后手动转类型优雅 10 倍。
测试:用 httpx 替代 requests
FastAPI 官方推荐用 httpx 做测试,支持异步:
import pytestfrom httpx import AsyncClient, ASGITransportfrom app.main import app
@pytest.fixtureasync def client(): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as ac: yield ac
@pytest.mark.anyioasync def test_create_book(client: AsyncClient): response = await client.post("/books", json={ "title": "Python 编程导论", "author": "John Guttag", "price": 89.00, "tags": ["Python", "入门"] }) assert response.status_code == 201 data = response.json() assert data["title"] == "Python 编程导论" assert "id" in data assert "created_at" in data
@pytest.mark.anyioasync def test_get_nonexistent_book(client: AsyncClient): response = await client.get("/books/99999") assert response.status_code == 404测试不需要启动真实服务器——ASGITransport 直接在内存中模拟请求,速度飞快。
性能调优:几个立竿见影的技巧
1. 用 async def 还是 def?
# ✅ I/O 密集型(数据库查询、HTTP 请求)—— 用 async def@app.get("/books")async def list_books(): books = await db.fetch_all("SELECT * FROM books") return books
# ✅ CPU 密集型(图片处理、计算)—— 用普通 def# FastAPI 会自动放到线程池执行,不阻塞事件循环@app.get("/compute")def heavy_computation(): result = sum(i * i for i in range(10_000_000)) return {"result": result}常见坑:在 async def 里调用同步阻塞函数(比如 requests.get())。这会阻塞整个事件循环!要么用 httpx(异步),要么用 run_in_executor 包装。
2. 响应模型优化
# ❌ 每次都序列化所有字段@app.get("/books", response_model=list[BookResponse])async def list_books(): ...
# ✅ 列表接口返回精简模型class BookSummary(BaseModel): id: int title: str author: str price: float
@app.get("/books", response_model=list[BookSummary])async def list_books(): ...列表接口不需要返回所有字段。减少序列化开销,减少网络传输,前端也不用处理一堆用不到的数据。
3. 生产部署命令
# 开发uvicorn app.main:app --reload --port 8000
# 生产(多 worker)uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
# 更高性能:用 gunicorn + uvicorn workergunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000快速容器化
FastAPI 项目天然适合 Docker 部署。一个精简的 Dockerfile:
FROM python:3.12-slim
WORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt
COPY ./app ./app
EXPOSE 8000CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]docker build -t bookstore-api .docker run -d -p 8000:8000 --name bookstore bookstore-api5 分钟就能跑起来。生产环境加上 --restart=always 和健康检查就行。
和 Flask 的真实对比
最后说点主观判断。我同时用过 Flask 和 FastAPI 做项目,体感差异:
| 维度 | Flask | FastAPI |
|---|---|---|
| 学习曲线 | 极低(太简单以至于什么都要自己装) | 低(约定多,但省事) |
| 类型安全 | 无(靠自觉) | 原生支持(Pydantic) |
| API 文档 | 需要 flask-restx/flasgger | 自动生成 |
| 异步支持 | Flask 2.0 后勉强支持 | 原生 ASGI,一等公民 |
| 生态 | 巨大(但很多库年久失修) | 快速增长(现代库为主) |
| 适合场景 | 简单页面、小工具 | API 服务、微服务、数据平台 |
一句话总结:2026 年新项目,选 FastAPI。Flask 不是不行,但 FastAPI 在类型安全、性能和开发体验上全面领先。
总结
FastAPI 的核心理念可以浓缩成三个词:类型驱动、约定优先、性能至上。
- 类型标注不是装饰——它驱动了校验、文档、序列化整条链路
- 依赖注入不是过度设计——它让认证、权限、数据库连接变得可测试、可组合
- 异步不是可选项——在 I/O 密集的 API 场景下,
async/await带来的并发能力是质的飞跃
如果你是前端开发者想学后端,FastAPI 是最短路径。如果你是 Python 开发者还在用 Flask,是时候升级了。
下一篇我们聊聊如何用 SQLAlchemy + Alembic 给这个书店 API 接上真正的数据库。Stay tuned 🚀
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!