fastapi-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

When to use

适用场景

Use this skill when working with FastAPI code. It teaches current best practices and prevents common mistakes that AI agents make with outdated patterns.
在编写FastAPI代码时使用本技能。它会传授当前的最佳实践,避免AI Agent因使用过时模式而犯的常见错误。

Critical Rules

核心规则

1. Use async def for I/O-bound endpoints, def for CPU-bound

1. I/O密集型端点使用async def,CPU密集型端点使用def

Wrong (agents do this):
python
@app.get("/users")
def get_users():
    users = db.query(User).all()
    return users

@app.get("/data")
async def get_data():
    result = heavy_computation()
    return result
Correct:
python
@app.get("/users")
async def get_users(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User))
    return result.scalars().all()

@app.get("/data")
def get_data():
    return heavy_computation()
Why: FastAPI runs async endpoints in the event loop; sync endpoints run in a thread pool. Use async for I/O (DB, HTTP, file) to avoid blocking. Use def for CPU-bound work; making it async would block the event loop.
错误示例(Agent常犯):
python
@app.get("/users")
def get_users():
    users = db.query(User).all()
    return users

@app.get("/data")
async def get_data():
    result = heavy_computation()
    return result
正确示例:
python
@app.get("/users")
async def get_users(db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User))
    return result.scalars().all()

@app.get("/data")
def get_data():
    return heavy_computation()
原因: FastAPI在事件循环中运行异步端点;同步端点在线程池中运行。I/O操作(数据库、HTTP、文件)使用异步方式可避免阻塞。CPU密集型工作使用def定义,若设为异步会阻塞事件循环。

2. Use Depends() for dependency injection

2. 使用Depends()实现依赖注入

Wrong (agents do this):
python
db = get_database()

@app.get("/items")
async def get_items():
    return db.query(Item).all()
Correct:
python
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/items")
async def get_items(db: Annotated[Session, Depends(get_db)]):
    return db.query(Item).all()
Why: Global DB connections leak, are not testable, and bypass FastAPI's dependency system. Depends() provides proper scoping, cleanup, and test overrides.
错误示例(Agent常犯):
python
db = get_database()

@app.get("/items")
async def get_items():
    return db.query(Item).all()
正确示例:
python
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/items")
async def get_items(db: Annotated[Session, Depends(get_db)]):
    return db.query(Item).all()
原因: 全局数据库连接会泄漏、无法测试,且绕过FastAPI的依赖系统。Depends()提供了合适的作用域、清理机制和测试覆盖能力。

3. Use Pydantic v2 patterns

3. 使用Pydantic v2模式

Wrong (agents do this):
python
from pydantic import validator

class Item(BaseModel):
    name: str
    price: float

    class Config:
        orm_mode = True

    @validator("price")
    def price_positive(cls, v):
        if v <= 0:
            raise ValueError("must be positive")
        return v
Correct:
python
from pydantic import BaseModel, field_validator, ConfigDict

class Item(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    name: str
    price: float

    @field_validator("price")
    @classmethod
    def price_positive(cls, v: float) -> float:
        if v <= 0:
            raise ValueError("must be positive")
        return v
Why: Pydantic v1 validator, Config, and orm_mode are deprecated. Use field_validator, model_validator, ConfigDict, and from_attributes.
错误示例(Agent常犯):
python
from pydantic import validator

class Item(BaseModel):
    name: str
    price: float

    class Config:
        orm_mode = True

    @validator("price")
    def price_positive(cls, v):
        if v <= 0:
            raise ValueError("must be positive")
        return v
正确示例:
python
from pydantic import BaseModel, field_validator, ConfigDict

class Item(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    name: str
    price: float

    @field_validator("price")
    @classmethod
    def price_positive(cls, v: float) -> float:
        if v <= 0:
            raise ValueError("must be positive")
        return v
原因: Pydantic v1中的validator、Config和orm_mode已被弃用。请使用field_validator、model_validator、ConfigDict和from_attributes。

4. Use lifespan context manager

4. 使用lifespan上下文管理器

Wrong (agents do this):
python
@app.on_event("startup")
async def startup():
    app.state.db = await create_pool()

@app.on_event("shutdown")
async def shutdown():
    await app.state.db.close()
Correct:
python
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.db = await create_pool()
    yield
    await app.state.db.close()

app = FastAPI(lifespan=lifespan)
Why: on_event is deprecated. The lifespan context manager gives a single place for startup and shutdown with proper resource ordering.
错误示例(Agent常犯):
python
@app.on_event("startup")
async def startup():
    app.state.db = await create_pool()

@app.on_event("shutdown")
async def shutdown():
    await app.state.db.close()
正确示例:
python
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.db = await create_pool()
    yield
    await app.state.db.close()

app = FastAPI(lifespan=lifespan)
原因: on_event已被弃用。lifespan上下文管理器将启动和关闭逻辑集中在一处,并确保资源的正确顺序处理。

5. Use BackgroundTasks for fire-and-forget work

5. 使用BackgroundTasks处理“即发即弃”任务

Wrong (agents do this):
python
@app.post("/send-email")
async def send_email(email: str):
    asyncio.create_task(send_email_async(email))
    return {"status": "queued"}
Correct:
python
@app.post("/send-email")
async def send_email(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_email_async, email)
    return {"status": "queued"}
Why: asyncio.create_task can outlive the request and is not awaited on shutdown. BackgroundTasks runs after the response is sent and is tied to the request lifecycle.
错误示例(Agent常犯):
python
@app.post("/send-email")
async def send_email(email: str):
    asyncio.create_task(send_email_async(email))
    return {"status": "queued"}
正确示例:
python
@app.post("/send-email")
async def send_email(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_email_async, email)
    return {"status": "queued"}
原因: asyncio.create_task创建的任务可能会在请求结束后继续存在,且在关闭时不会被等待。BackgroundTasks会在响应发送后执行,且与请求生命周期绑定。

6. Use APIRouter for route organization

6. 使用APIRouter组织路由

Wrong (agents do this):
python
undefined
错误示例(Agent常犯):
python
undefined

main.py - 500 lines of routes

main.py - 500行路由代码

@app.get("/users") @app.get("/users/{id}") @app.post("/items") @app.get("/items")

**Correct:**

```python
@app.get("/users") @app.get("/users/{id}") @app.post("/items") @app.get("/items")

**正确示例:**

```python

main.py

main.py

app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(items.router, prefix="/items", tags=["items"])
app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(items.router, prefix="/items", tags=["items"])

routers/users.py

routers/users.py

router = APIRouter() @router.get("/") @router.get("/{id}")

**Why:** Single-file apps become unmaintainable. APIRouter enables routers/, models/, services/,
dependencies/ structure.
router = APIRouter() @router.get("/") @router.get("/{id}")

**原因:** 单文件应用会变得难以维护。APIRouter支持将代码拆分为routers/、models/、services/、dependencies/等结构。

7. Use response_model for output validation

7. 使用response_model进行输出验证

Wrong (agents do this):
python
@app.get("/items/{id}")
async def get_item(id: int):
    item = await db.get(Item, id)
    return {"id": item.id, "name": item.name}
Correct:
python
@app.get("/items/{id}", response_model=ItemOut)
async def get_item(id: int, db: Session = Depends(get_db)):
    item = await db.get(Item, id)
    if not item:
        raise HTTPException(status_code=404)
    return item
Why: Raw dicts bypass validation and OpenAPI. response_model ensures schema consistency, serialization, and docs.
错误示例(Agent常犯):
python
@app.get("/items/{id}")
async def get_item(id: int):
    item = await db.get(Item, id)
    return {"id": item.id, "name": item.name}
正确示例:
python
@app.get("/items/{id}", response_model=ItemOut)
async def get_item(id: int, db: Session = Depends(get_db)):
    item = await db.get(Item, id)
    if not item:
        raise HTTPException(status_code=404)
    return item
原因: 直接返回字典会绕过验证和OpenAPI文档。response_model确保了 schema 一致性、序列化和文档的准确性。

8. Use status codes from fastapi.status

8. 使用fastapi.status中的状态码

Wrong (agents do this):
python
raise HTTPException(status_code=404, detail="Not found")
raise HTTPException(status_code=401, detail="Unauthorized")
Correct:
python
from fastapi import status

raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
Why: Magic numbers are error-prone. status constants are self-documenting and match HTTP spec.
错误示例(Agent常犯):
python
raise HTTPException(status_code=404, detail="Not found")
raise HTTPException(status_code=401, detail="Unauthorized")
正确示例:
python
from fastapi import status

raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized")
原因: 魔法数字容易出错。status常量具有自描述性,且符合HTTP规范。

9. Use Annotated for dependencies

9. 使用Annotated定义依赖

Wrong (agents do this):
python
@app.get("/me")
async def read_me(current_user: User = Depends(get_current_user)):
    return current_user
Correct:
python
@app.get("/me")
async def read_me(current_user: Annotated[User, Depends(get_current_user)]):
    return current_user
Why: Annotated is the recommended FastAPI pattern. It keeps types and dependencies in one place and supports dependency reuse.
错误示例(Agent常犯):
python
@app.get("/me")
async def read_me(current_user: User = Depends(get_current_user)):
    return current_user
正确示例:
python
@app.get("/me")
async def read_me(current_user: Annotated[User, Depends(get_current_user)]):
    return current_user
原因: Annotated是FastAPI推荐的模式。它将类型和依赖定义放在一处,支持依赖复用。

10. Use pydantic-settings for configuration

10. 使用pydantic-settings管理配置

Wrong (agents do this):
python
import os
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///db.sqlite")
Correct:
python
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str = "sqlite:///db.sqlite"
    debug: bool = False
    model_config = {"env_file": ".env"}

settings = Settings()
Why: os.getenv has no validation or typing. BaseSettings provides validation, .env loading, and type safety.
错误示例(Agent常犯):
python
import os
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///db.sqlite")
正确示例:
python
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str = "sqlite:///db.sqlite"
    debug: bool = False
    model_config = {"env_file": ".env"}

settings = Settings()
原因: os.getenv不具备验证和类型检查能力。BaseSettings提供了验证、.env文件加载和类型安全的特性。

Patterns

推荐模式

  • Define routers in routers/ with prefix and tags
  • Put shared dependencies in dependencies.py
  • Use HTTPException with status constants for errors
  • Use Path, Query, Body, Header with validation (min_length, ge, le)
  • Register custom exception handlers with app.add_exception_handler
  • Use middleware sparingly; order matters (first added runs last for requests)
  • 在routers/目录下定义路由,并设置前缀和标签
  • 将共享依赖放在dependencies.py中
  • 使用带状态常量的HTTPException处理错误
  • 使用Path、Query、Body、Header进行验证(如min_length、ge、le)
  • 通过app.add_exception_handler注册自定义异常处理器
  • 谨慎使用中间件;执行顺序很重要(先添加的中间件在请求处理时最后执行)

Anti-Patterns

反模式

  • Do not use @app.on_event("startup") or @app.on_event("shutdown")
  • Do not use asyncio.create_task for request-scoped background work
  • Do not use global variables for DB, cache, or config
  • Do not use Pydantic v1 @validator or class Config
  • Do not return raw dicts without response_model
  • Do not use magic numbers for status codes
  • Do not put all routes in main.py
  • 不要使用@app.on_event("startup")或@app.on_event("shutdown")
  • 不要使用asyncio.create_task处理请求作用域的后台任务
  • 不要使用全局变量存储数据库、缓存或配置
  • 不要使用Pydantic v1的@validator或class Config
  • 不要在未指定response_model的情况下直接返回字典
  • 不要使用魔法数字作为状态码
  • 不要将所有路由都放在main.py中