Fastapi开发规范
路由、Pydantic 模型、依赖注入、中间件
- [Requirement] 采用分层目录结构,按职责划分包:
api(路由层)、schemas(Pydantic 模型)、services(业务逻辑)、repositories(数据访问)、core(配置/异常/安全)、models(ORM 模型)。 - [Requirement] 入口文件
app/main.py只负责创建FastAPI实例、注册路由与中间件,不承载业务逻辑。 - [Standard] 路由按资源域拆分,每个域一个模块或包(
app/api/v1/users.py),通过APIRouter聚合,在main.py中统一include_router。 - [Standard] 配置集中在
app/core/config.py,使用pydantic-settings从环境变量读取,禁止在代码中硬编码配置项。 - [Standard] 常量(错误码、状态枚举)集中在
app/core/constants.py,禁止散落魔法值。 - [Suggestion] 大型项目按 DDD 思路拆分子包
app/domains/user/,每个域内自带api/schemas/services/repositories。
myapp/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 入口
│ ├── api/
│ │ ├── __init__.py
│ │ └── v1/
│ │ ├── __init__.py
│ │ ├── router.py # v1 总路由聚合
│ │ └── users.py # 用户相关路由
│ ├── schemas/ # Pydantic 模型
│ │ ├── user.py
│ │ └── common.py
│ ├── services/ # 业务逻辑
│ │ └── user_service.py
│ ├── repositories/ # 数据访问
│ │ └── user_repo.py
│ ├── models/ # ORM 模型
│ │ └── user.py
│ ├── core/
│ │ ├── config.py # 配置
│ │ ├── exceptions.py # 自定义异常
│ │ └── security.py # 鉴权
│ └── db/
│ └── session.py # 数据库会话
├── tests/
├── pyproject.toml
└── .env.example# app/main.py
from fastapi import FastAPI
from app.api.v1.router import api_router
from app.core.config import settings
app = FastAPI(title=settings.app_name, version="1.0.0")
app.include_router(api_router, prefix="/api/v1")# 反模式:main.py 堆砌业务逻辑与数据库操作
@app.get("/users")
def get_users(db: Session = Depends(get_db)):
users = db.query(User).all()
return [{"id": u.id, "name": u.name} for u in users] # 逻辑全堆在路由- [Requirement] URL 使用名词复数表示资源(
/users、/orders),禁止在 URL 中出现动词(/getUsers)。 - [Requirement] HTTP 方法语义正确:
GET查询、POST创建、PUT整体更新、PATCH部分更新、DELETE删除。 - [Requirement] 所有路由必须标注
response_model,利用 Pydantic 自动过滤输出字段,避免敏感字段泄露。 - [Standard] 路由函数只做参数解析与响应组装,业务逻辑下沉到
service层;路由函数体不超过 20 行。 - [Standard] 路径参数做类型约束(
user_id: int),查询参数用 PydanticQuery限制范围,避免无效请求透传到服务层。 - [Default] 统一响应结构:成功返回业务数据,失败由全局异常处理器返回统一错误体;禁止在路由内手动拼
{"code": 0, "data": ...}。 - [Suggestion] 为每个路由添加
summary与description,生成清晰的 OpenAPI 文档。
# app/api/v1/users.py
from fastapi import APIRouter, Depends, HTTPException, status
from app.schemas.user import UserCreate, UserOut
from app.services.user_service import UserService
router = APIRouter(prefix="/users", tags=["users"])
@router.get(
"/{user_id}",
response_model=UserOut,
summary="获取用户详情",
)
def get_user(
user_id: int,
service: UserService = Depends(),
) -> UserOut:
user = service.get_by_id(user_id)
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user
@router.post(
"",
response_model=UserOut,
status_code=status.HTTP_201_CREATED,
summary="创建用户",
)
def create_user(payload: UserCreate, service: UserService = Depends()) -> UserOut:
return service.create(payload)# app/schemas/common.py — 统一响应结构
from typing import Any, Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
code: int = 0
message: str = "success"
data: T | None = None# 反模式:URL 含动词、方法语义错误
@router.get("/getUserById/{id}") # 动词 + 路径参数未约束类型
def get_user(id, db): # 无类型注解
user = db.query(User).get(id)
return {"code": 0, "data": user} # 手动拼响应体,泄露密码字段
# 反模式:路由内直接写 SQL 与业务逻辑
@router.post("/createUser")
def create_user(name: str, email: str, db):
if not name or "@" not in email:
return {"error": "invalid"} # 非统一错误结构
db.execute(f"INSERT INTO users ...") # SQL 注入风险
db.commit()- [Requirement] 所有请求体与响应体必须定义 Pydantic 模型,禁止直接用
dict接收或返回。 - [Requirement] 请求模型与响应模型分离:创建用
XxxCreate、更新用XxxUpdate、响应用XxxOut,避免复用导致字段语义混乱。 - [Standard] 使用 Pydantic v2 的
Field对字段做约束(长度、范围、正则),把校验前置到模型层。 - [Standard] 敏感字段(密码、token)仅出现在请求模型中,响应模型必须用
exclude或不声明该字段来过滤。 - [Standard] 枚举字段使用
enum.Enum+ Pydantic 校验,禁止在业务代码里散落字符串判断。 - [Default] 启用
model_config = ConfigDict(str_strip_whitespace=True)自动去除字符串首尾空格。 - [Caution] 响应模型不要复用 ORM 模型类,必须独立定义,防止数据库字段变更直接影响 API 契约。
# app/schemas/user.py
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class UserStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
class UserBase(BaseModel):
name: str = Field(min_length=1, max_length=50)
email: EmailStr
status: UserStatus = UserStatus.ACTIVE
class UserCreate(UserBase):
password: str = Field(min_length=8, max_length=128)
class UserUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=50)
status: UserStatus | None = None
class UserOut(UserBase):
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
# 注意:不包含 password 字段,自动过滤# 反模式:直接用 dict 接收,无校验
@router.post("/users")
def create_user(payload: dict):
return {"id": 1, **payload} # 无字段约束、无类型校验、可能泄露敏感字段
# 反模式:请求响应复用同一个模型
class User(BaseModel):
id: int
name: str
password: str # 响应时泄露密码- [Requirement] 数据库会话、配置、鉴权用户等横切依赖必须通过
Depends()注入,禁止在函数内部手动new或全局单例。 - [Requirement]
get_db依赖必须使用yield语法,确保请求结束时会话自动关闭/回滚。 - [Standard] Service/Repository 类实现为可注入依赖(构造函数参数可被 FastAPI 解析),通过
Depends(UserService)注入,避免在路由内service = UserService(db)。 - [Standard] 鉴权依赖(如
get_current_user)在需要登录的路由上统一注入,禁止每个路由手动解析 token。 - [Default] 复用的依赖用
Depends(dep, use_cache=True)(默认即开启),同请求内只执行一次。 - [Suggestion] 跨多个路由的公共依赖(如分页参数、租户上下文)封装为独立依赖函数,减少重复代码。
# app/db/session.py
from collections.abc import Generator
from sqlalchemy.orm import Session
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()# app/core/security.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
payload = decode_token(token)
if not payload:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
return get_user_by_id(payload["sub"])# app/services/user_service.py
class UserService:
def __init__(self, db: Session = Depends(get_db)) -> None:
self.db = db
def get_by_id(self, user_id: int) -> User | None:
return self.db.query(User).get(user_id)# 路由中使用
@router.get("/me", response_model=UserOut)
def get_me(current_user: User = Depends(get_current_user)) -> UserOut:
return current_user# 反模式:在路由内手动创建会话与服务,未注入
@router.get("/users/{user_id}")
def get_user(user_id: int):
db = SessionLocal() # 未注入、无关闭保证
service = UserService(db)
user = service.get_by_id(user_id)
db.close() # 异常时不会执行
return user
# 反模式:每个路由手动解析 token
@router.get("/me")
def get_me(authorization: str = Header(...)):
token = authorization.split(" ")[1] # 重复解析逻辑
...- [Requirement] 全局中间件用于横切关注点(请求日志、CORS、异常兜底、请求 ID 注入),业务逻辑禁止写在中间件里。
- [Requirement] CORS 必须显式配置允许的源、方法、头,生产环境禁止
allow_origins=["*"]配合凭证。 - [Standard] 使用
@app.middleware("http")或自定义BaseHTTPMiddleware实现中间件,优先用官方中间件类。 - [Standard] 中间件内对
request.state注入上下文(如request_id),通过request.url记录访问日志,禁止用print。 - [Default] 请求日志中间件记录:请求 ID、方法、路径、状态码、耗时;敏感请求体(密码、token)禁止记录。
- [Caution] 中间件执行顺序很重要:CORS 中间件必须在最外层,异常处理中间件在日志中间件内侧,确保异常被正确转换并记录。
- [Caution] 中间件内禁止做数据库写入或耗时操作,避免拖慢所有请求。
# app/main.py
import time
import uuid
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
app = FastAPI(title=settings.app_name)
# 1. CORS 最外层
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
# 2. 请求 ID + 耗时日志中间件
@app.middleware("http")
async def request_logger(request: Request, call_next):
request_id = str(uuid.uuid4())
request.state.request_id = request_id
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
response.headers["X-Request-ID"] = request_id
logger.info(
"request",
request_id=request_id,
method=request.method,
path=request.url.path,
status=response.status_code,
elapsed_ms=round(elapsed * 1000, 2),
)
return response# app/core/exceptions.py — 全局异常处理器
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class BizError(Exception):
def __init__(self, code: int, message: str) -> None:
self.code = code
self.message = message
def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(BizError)
async def biz_error_handler(_: Request, exc: BizError) -> JSONResponse:
return JSONResponse(
status_code=200,
content={"code": exc.code, "message": exc.message, "data": None},
)# 反模式:生产环境 CORS 全开 + 允许凭证
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 安全风险
allow_credentials=True, # 与 * 冲突,浏览器会拒绝
allow_methods=["*"],
allow_headers=["*"],
)
# 反模式:中间件写业务逻辑 + 打印敏感信息
@app.middleware("http")
async def bad_middleware(request, call_next):
body = await request.body()
print(body) # 泄露密码/token,且消耗请求体导致后续无法读取
if request.url.path == "/admin":
... # 业务逻辑不应在中间件
return await call_next(request)