| name | fastapi-style |
| description | FastAPI 框架编码风格 Skill。蒸馏自 tiangolo/fastapi 源码、Sebastián Ramírez 的设计哲学、
官方文档最佳实践、Pydantic v2 迁移指南以及 FastAPI 核心贡献者的 PR review 习惯。
触发词:「FastAPI 风格」「tiangolo 风格」「fastapi style」「Pydantic 最佳实践」「FastAPI API 设计」。
适用:FastAPI Web 服务开发、REST API 设计、Pydantic 模型建模、依赖注入系统、OpenAPI 文档生成。
|
FastAPI · 编码 DNA
"FastAPI is designed to be easy to use, but also to make it hard to make mistakes." — Sebastián Ramírez
"Pydantic is not a validation library. It's a data modeling library that validates." — Samuel Colvin
角色定义
此 Skill 激活后,你写出的代码应该让 FastAPI 核心 contributor 在 code review 时感觉
「这代码把 Pydantic、依赖注入和 OpenAPI 三者用得浑然一体」,
而不是「这是 Flask 开发者用 FastAPI 写的 Flask」。
这意味着:类型注解驱动、依赖注入优先、Pydantic 模型精确建模、OpenAPI 文档是一等公民。
命名 DNA
5 条直觉规则:
-
Pydantic 模型名用 PascalCase,并体现领域语义(不叫 Data 或 Schema)
class UserCreate(BaseModel):
email: EmailStr
password: SecretStr
class UserPublic(BaseModel):
id: int
email: EmailStr
created_at: datetime
class UserUpdate(BaseModel):
email: EmailStr | None = None
display_name: str | None = None
class UserData(BaseModel): ...
class UserSchema(BaseModel): ...
class User(BaseModel): ...
-
路由函数名用 动词_名词 snake_case,准确描述操作
@router.post("/users", response_model=UserPublic)
async def create_user(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
...
@router.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
...
@router.patch("/users/{user_id}", response_model=UserPublic)
async def update_user(user_id: int, user_in: UserUpdate, db: AsyncSession = Depends(get_db)):
...
@router.post("/users")
async def users(data: dict):
...
@router.get("/users/{id}")
async def user_handler(id: int):
...
-
依赖函数命名:get_ 前缀表示资源获取,require_ 前缀表示带校验的守卫
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with SessionLocal() as session:
yield session
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
...
async def require_admin(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin required")
return current_user
async def db_session(): ...
async def check_user(): ...
-
路由路径用复数名词,路径参数用 snake_case
@router.get("/users")
@router.get("/users/{user_id}")
@router.get("/users/{user_id}/orders")
@router.post("/users/{user_id}/activate")
@router.get("/user")
@router.get("/users/{userId}")
@router.get("/getUser")
-
settings/config 用 pydantic-settings 的 BaseSettings,不用裸 os.environ
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_ignore_empty=True)
database_url: str
secret_key: SecretStr
debug: bool = False
max_connections: int = 10
settings = Settings()
DATABASE_URL = os.getenv("DATABASE_URL")
SECRET_KEY = os.environ["SECRET_KEY"]
结构偏好
项目目录结构(中型项目标准布局):
app/
├── main.py # FastAPI app 创建、router 注册、lifespan
├── config.py # Settings(pydantic-settings)
├── dependencies.py # 全局共用依赖(get_db、get_current_user)
├── api/
│ ├── v1/
│ │ ├── router.py # v1 总路由(include_router 汇总)
│ │ ├── users.py # users 资源路由
│ │ └── orders.py # orders 资源路由
├── models/
│ ├── user.py # ORM 模型(SQLAlchemy)
│ └── order.py
├── schemas/ # 或叫 dto/ — Pydantic 模型
│ ├── user.py # UserCreate, UserPublic, UserUpdate
│ └── order.py
├── services/ # 业务逻辑层(不含路由、不含 DB 细节)
│ ├── user_service.py
│ └── order_service.py
└── db/
├── session.py # 数据库 session 工厂
└── base.py # ORM Base
Pydantic v2 模型设计原则:
from pydantic import BaseModel, ConfigDict, Field, EmailStr
class UserPublic(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: EmailStr
display_name: str = Field(min_length=1, max_length=100)
created_at: datetime
class UserCreate(BaseModel):
email: EmailStr
password: str = Field(min_length=8)
confirm_password: str
@model_validator(mode='after')
def passwords_match(self) -> 'UserCreate':
if self.password != self.confirm_password:
raise ValueError('passwords do not match')
return self
class OldUser(BaseModel):
class Config:
orm_mode = True
@validator('email')
def validate_email(cls, v): ...
依赖注入分层:路由层薄,服务层厚
@router.post("/users", response_model=UserPublic, status_code=201)
async def create_user(
user_in: UserCreate,
db: AsyncSession = Depends(get_db),
) -> UserPublic:
user = await UserService(db).create(user_in)
return user
class UserService:
def __init__(self, db: AsyncSession) -> None:
self.db = db
async def create(self, user_in: UserCreate) -> User:
if await self._email_exists(user_in.email):
raise HTTPException(status_code=409, detail="Email already registered")
hashed = hash_password(user_in.password)
user = User(email=user_in.email, hashed_password=hashed)
self.db.add(user)
await self.db.commit()
await self.db.refresh(user)
return user
async def _email_exists(self, email: str) -> bool:
result = await self.db.execute(
select(User).where(User.email == email)
)
return result.scalar_one_or_none() is not None
@router.post("/users")
async def create_user(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
existing = await db.execute(select(User).where(User.email == user_in.email))
if existing.scalar_one_or_none():
raise HTTPException(...)
lifespan 管理应用生命周期(替代 on_event):
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
await db_pool.connect()
await redis_client.ping()
logger.info("application started")
yield
await db_pool.disconnect()
await redis_client.aclose()
logger.info("application stopped")
app = FastAPI(lifespan=lifespan)
@app.on_event("startup")
async def startup(): ...
@app.on_event("shutdown")
async def shutdown(): ...
OpenAPI 文档是一等公民:每个路由都要有完整注释
@router.post(
"/users",
response_model=UserPublic,
status_code=201,
summary="创建新用户",
description="注册新用户账号,邮箱需唯一。密码会自动 bcrypt 哈希存储,不会明文保存。",
responses={
201: {"description": "用户创建成功"},
409: {"description": "邮箱已被注册", "model": ErrorResponse},
422: {"description": "请求体格式错误"},
},
tags=["users"],
)
async def create_user(
user_in: UserCreate = Body(..., examples={
"standard": {
"summary": "普通注册",
"value": {"email": "user@example.com", "password": "securepass123"},
}
}),
db: AsyncSession = Depends(get_db),
) -> UserPublic:
...
@router.post("/users")
async def create_user(data: dict, db=Depends(get_db)):
...
注释哲学
docstring 面向使用者,解释业务语义而不是代码逻辑:
class UserService:
"""用户领域服务。
负责用户账号的创建、查询、更新和鉴权逻辑。
所有数据库操作通过注入的 AsyncSession 进行,
不直接管理事务(由调用方或中间件管理)。
"""
async def create(self, user_in: UserCreate) -> User:
"""创建新用户账号。
Args:
user_in: 用户注册信息,包含 email 和明文密码。
Returns:
创建成功的 User ORM 对象(已 refresh,包含 DB 生成的 id 和 created_at)。
Raises:
HTTPException(409): 邮箱已被注册。
HTTPException(500): 数据库写入失败。
"""
类型注解优先于 docstring 解释参数类型:
async def get_user_by_email(email: EmailStr, db: AsyncSession) -> User | None:
"""通过邮箱查找用户,不存在时返回 None(不抛异常)。"""
async def get_user_by_email(email: str, db) -> User:
"""
Args:
email (str): 用户邮箱字符串
db (AsyncSession): 数据库会话对象
Returns:
User: 用户对象
"""
反模式(绝不这样写)
-
路由函数接收 dict 或 Any(丢失类型校验和 OpenAPI 文档)
@router.post("/users")
async def create_user(body: dict): ...
@router.post("/users")
async def create_user(user_in: UserCreate): ...
-
在 Pydantic 模型里暴露 ORM 内部字段(信息泄露)
@router.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)) -> User:
return await db.get(User, user_id)
@router.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)) -> UserPublic:
user = await db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
-
把数据库会话作为全局变量(多并发下线程/协程安全问题)
db = SessionLocal()
@router.get("/users")
async def list_users():
return db.query(User).all()
@router.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User))
return result.scalars().all()
-
HTTPException 的 detail 暴露内部错误信息(安全问题)
try:
await db.commit()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
try:
await db.commit()
except Exception:
logger.exception("database commit failed")
raise HTTPException(status_code=500, detail="Internal server error")
-
response_model 和函数返回类型注解不一致
@router.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int) -> User:
...
@router.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int) -> UserPublic:
...
-
同步阻塞操作在 async 路由里直接调用(阻塞事件循环)
@router.get("/proxy")
async def proxy():
response = requests.get("https://api.example.com/data")
return response.json()
@router.get("/proxy")
async def proxy(client: httpx.AsyncClient = Depends(get_http_client)):
response = await client.get("https://api.example.com/data")
return response.json()
-
依赖链里有副作用但没有用 yield(资源泄漏)
async def get_db() -> AsyncSession:
db = SessionLocal()
return db
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with SessionLocal() as db:
try:
yield db
await db.commit()
except Exception:
await db.rollback()
raise
校验测试
- 类型覆盖率:路由函数的所有参数(路径参数、Query、Body、Depends)有没有显式类型注解?
dict 和 Any 应该是零。
- OpenAPI 完整性:在
/docs 页面看每个路由,有没有 summary、正确的 response_model、错误码的 responses?能不能直接在 Swagger UI 里测试?
- 依赖隔离:路由函数是不是「薄」的(<10行)?业务逻辑有没有下沉到服务层或依赖函数里?
来源