跳轉到

OpenAPI 與 FastAPI 最佳實踐

架構設計原則

API 設計模式

模式 說明 適用場景
資源導向 以資源為中心設計端點 CRUD 操作、RESTful API
動作導向 以操作為中心設計端點 複雜業務流程、RPC 風格 API
混合模式 結合資源和動作 大型系統、多樣化需求
# 資源導向示例
@app.get("/users/{user_id}")
async def read_user(user_id: int):
    return {"id": user_id, "name": "Alice"}

# 動作導向示例
@app.post("/process-payment/")
async def process_payment(payment: PaymentModel):
    return {"status": "processed", "transaction_id": "123"}

模組化設計

使用 APIRouter 組織大型 API:

from fastapi import APIRouter, FastAPI

app = FastAPI()

# 用戶相關路由
user_router = APIRouter(prefix="/users", tags=["users"])

@user_router.get("/{user_id}")
async def read_user(user_id: int):
    return {"id": user_id, "name": "Alice"}

# 註冊路由
app.include_router(user_router)

版本控制策略

策略 實現方式 優點 缺點
URL 路徑 /v1/users/ 簡單直觀 URL 變長
查詢參數 /users/?version=1 不改變資源路徑 可選性導致複雜性
標頭 X-API-Version: 1 保持 URL 清潔 不易於瀏覽器直接訪問
內容協商 Accept: application/vnd.api+json;version=1 符合 HTTP 標準 較複雜
# URL 路徑版本控制
@app.get("/v1/users/")
async def read_users_v1():
    return [{"id": 1, "name": "Alice"}]

# v2 API 添加了更多字段
@app.get("/v2/users/")
async def read_users_v2():
    return [{"id": 1, "name": "Alice", "email": "alice@example.com"}]

數據模型最佳實踐

模型設計原則

原則 說明 示例
單一職責 每個模型專注於一個領域 分離 UserProfileUserCredentials
繼承與組合 利用繼承減少重複代碼 BaseItemBookItem, ElectronicItem
驗證邏輯內置 在模型中內置驗證邏輯 使用 Pydantic 驗證器
文檔友好 添加清晰的字段描述和示例 使用 Fielddescriptionexample
from pydantic import BaseModel, EmailStr, Field, field_validator

class UserBase(BaseModel):
    username: str = Field(..., min_length=3, example="johndoe")
    email: EmailStr = Field(..., example="john@example.com")

class UserCreate(UserBase):
    password: str = Field(..., min_length=8)

    @field_validator("password")
    def password_strength(cls, v):
        if not any(char.isdigit() for char in v):
            raise ValueError("密碼必須包含至少一個數字")
        return v

class User(UserBase):
    id: int = Field(..., example=1)
    is_active: bool = True

請求與響應模型分離

為不同操作設計專用模型:

# 請求模型
class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

class UserUpdate(BaseModel):
    email: EmailStr = None
    full_name: str = None

# 響應模型
class UserResponse(BaseModel):
    id: int
    username: str
    email: EmailStr
    is_active: bool

@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
    # 創建用戶邏輯
    return {
        "id": 1,
        "username": user.username,
        "email": user.email,
        "is_active": True
    }

通用模式與模型

常用模型模式:

from typing import Generic, List, TypeVar
from pydantic.generics import GenericModel

# 分頁響應
T = TypeVar('T')

class Page(GenericModel, Generic[T]):
    items: List[T]
    total: int
    page: int
    size: int

# 使用示例
@app.get("/users/", response_model=Page[User])
async def read_users(page: int = 1, size: int = 10):
    users = [{"id": i, "username": f"user{i}", "email": f"user{i}@example.com", "is_active": True} 
             for i in range(1, 11)]
    return {
        "items": users,
        "total": 100,
        "page": page,
        "size": size
    }

路徑操作最佳實踐

HTTP 方法使用指南

HTTP 方法 用途 示例
GET 獲取資源 GET /users/ 獲取用戶列表
POST 創建資源 POST /users/ 創建新用戶
PUT 全量更新資源 PUT /users/123 更新整個用戶
PATCH 部分更新資源 PATCH /users/123 更新部分用戶字段
DELETE 刪除資源 DELETE /users/123 刪除用戶
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    """創建新項目 (201 Created)"""
    return {"id": 1, **item.model_dump()}

@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int):
    """刪除項目 (204 No Content)"""
    if item_id != 1:
        raise HTTPException(status_code=404, detail="Item not found")
    return Response(status_code=status.HTTP_204_NO_CONTENT)

路徑參數與查詢參數

使用指南:

參數類型 適用場景 示例
路徑參數 資源標識符 /users/{user_id}
查詢參數 過濾、排序、分頁 /users/?role=admin&sort=name
@app.get("/users/{user_id}")
async def read_user(
    user_id: int = Path(..., title="用戶 ID", ge=1),
    include_inactive: bool = False
):
    """獲取特定用戶信息"""
    return {"user_id": user_id, "include_inactive": include_inactive}

@app.get("/users/")
async def read_users(
    skip: int = Query(0, ge=0),
    limit: int = Query(100, ge=1, le=1000),
    role: str = None,
    sort: List[str] = Query([])
):
    """獲取用戶列表"""
    return {
        "skip": skip,
        "limit": limit,
        "role": role,
        "sort": sort
    }

狀態碼使用指南

狀態碼 用途 FastAPI 實現
200 OK 成功獲取資源 默認 GET 響應
201 Created 成功創建資源 status_code=201
204 No Content 成功但無返回內容 status_code=204 + Response()
400 Bad Request 請求格式錯誤 驗證錯誤或 HTTPException(400)
401 Unauthorized 未認證 HTTPException(401)
403 Forbidden 權限不足 HTTPException(403)
404 Not Found 資源不存在 HTTPException(404)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 0:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid item ID"
        )
    elif item_id == 999:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Item not found"
        )
    return {"item_id": item_id, "name": "Example Item"}

安全與認證最佳實踐

認證方案對比

認證方案 適用場景 實現方式
API 密鑰 簡單的 API 訪問控制 請求標頭、查詢參數或 Cookie
OAuth2 密碼流 用戶名/密碼登錄 OAuth2PasswordBearer
JWT 令牌 無狀態身份驗證 OAuth2PasswordBearer + JWT 編碼/解碼
from fastapi.security import OAuth2PasswordBearer, APIKeyHeader

# OAuth2 密碼流
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    if form_data.username != "johndoe" or form_data.password != "secret":
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    return {"access_token": "fake_token", "token_type": "bearer"}

@app.get("/users/me")
async def read_users_me(token: str = Depends(oauth2_scheme)):
    return {"username": "johndoe", "token": token}

# API 密鑰
api_key_header = APIKeyHeader(name="X-API-Key")

@app.get("/items/")
async def read_items(api_key: str = Depends(api_key_header)):
    if api_key != "valid_api_key":
        raise HTTPException(status_code=401, detail="Invalid API Key")
    return [{"id": 1, "name": "Item 1"}]

JWT 認證實現

from datetime import datetime, timedelta
from jose import JWTError, jwt

# JWT 配置
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=401, detail="Invalid token")
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")
    return {"username": username}

@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    # 驗證用戶
    if form_data.username != "johndoe" or form_data.password != "secret":
        raise HTTPException(status_code=400, detail="Incorrect username or password")

    # 創建訪問令牌
    access_token = create_access_token(data={"sub": form_data.username})
    return {"access_token": access_token, "token_type": "bearer"}

權限控制

from fastapi import Security
from fastapi.security import SecurityScopes

oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="token",
    scopes={
        "users:read": "讀取用戶信息",
        "users:write": "修改用戶信息",
        "items:read": "讀取項目",
        "items:write": "創建或修改項目",
    },
)

async def get_current_user_with_scopes(
    security_scopes: SecurityScopes, 
    token: str = Depends(oauth2_scheme)
):
    # 簡化示例,實際應解析 JWT 令牌
    if token != "valid_token":
        raise HTTPException(status_code=401, detail="Invalid token")

    # 假設令牌包含這些範圍
    token_scopes = ["users:read", "items:read"]

    # 檢查所需範圍
    for scope in security_scopes.scopes:
        if scope not in token_scopes:
            raise HTTPException(
                status_code=403,
                detail=f"Not enough permissions. Required: {scope}",
            )

    return {"username": "johndoe", "scopes": token_scopes}

@app.get(
    "/users/", 
    dependencies=[Security(get_current_user_with_scopes, scopes=["users:read"])]
)
async def read_users():
    return [{"username": "johndoe"}]

@app.post(
    "/users/", 
    dependencies=[Security(get_current_user_with_scopes, scopes=["users:write"])]
)
async def create_user(username: str):
    return {"username": username}

錯誤處理最佳實踐

標準化錯誤響應

from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI()

class ErrorResponse(BaseModel):
    code: str
    message: str
    details: dict = None

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    return JSONResponse(
        status_code=422,
        content=ErrorResponse(
            code="VALIDATION_ERROR",
            message="資料驗證錯誤",
            details={"errors": exc.errors()}
        ).model_dump(),
    )

@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    return JSONResponse(
        status_code=exc.status_code,
        content=ErrorResponse(
            code=f"HTTP_{exc.status_code}",
            message=exc.detail,
            details=exc.headers if hasattr(exc, "headers") else None
        ).model_dump(),
    )

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 0:
        raise HTTPException(status_code=400, detail="Invalid item ID")
    if item_id == 999:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item_id": item_id, "name": "Sample Item"}

自定義異常

class NotFoundError(Exception):
    def __init__(self, resource_type: str, resource_id: str):
        self.resource_type = resource_type
        self.resource_id = resource_id
        self.message = f"{resource_type} with ID {resource_id} not found"
        super().__init__(self.message)

@app.exception_handler(NotFoundError)
async def not_found_exception_handler(request, exc):
    return JSONResponse(
        status_code=404,
        content=ErrorResponse(
            code="RESOURCE_NOT_FOUND",
            message=exc.message,
            details={
                "resource_type": exc.resource_type,
                "resource_id": exc.resource_id
            }
        ).model_dump(),
    )

@app.get("/users/{user_id}")
async def read_user(user_id: int):
    if user_id == 999:
        raise NotFoundError("User", str(user_id))
    return {"id": user_id, "name": "Sample User"}

總結

類別 最佳實踐
API 設計 使用資源導向設計、模組化路由、一致的版本控制
數據模型 分離請求/響應模型、使用繼承減少重複、添加完整驗證
路徑操作 正確使用 HTTP 方法、適當使用狀態碼、清晰的參數設計
安全性 實現 OAuth2/JWT 認證、精細的權限控制、安全的密碼處理
錯誤處理 標準化錯誤響應、自定義異常處理、完整的錯誤信息