本文 AI 產出,尚未審核
FastAPI 應用結構與啟動方式:多檔案拆分(routers / services / models)
簡介
在實務開發中,隨著功能需求不斷擴充,將 FastAPI 應用寫在單一檔案會很快變得難以維護。
將程式碼依照 路由 (router)、業務服務 (service)、資料模型 (model) 進行模組化拆分,不僅能提升可讀性,還能讓團隊協作更順暢、測試更方便。
本篇文章將說明如何以 多檔案結構 建立一個可擴充的 FastAPI 專案,並提供實作範例、常見陷阱與最佳實踐,讓 初學者到中級開發者 都能快速上手、在專案中落地。
核心概念
1. 為什麼要拆分檔案?
- 職責分離:路由只負責 HTTP 介面、服務層負責商業邏輯、模型層負責資料結構與驗證。
- 降低耦合:各模組之間僅透過介面(function / class)互相溝通,避免相互依賴導致的循環匯入。
- 方便測試:可以針對 service、model 單元測試,而不必啟動整個 API 伺服器。
2. 常見的專案目錄結構
my_fastapi_app/
│
├─ app/
│ ├─ __init__.py
│ ├─ main.py # 入口點
│ ├─ routers/
│ │ ├─ __init__.py
│ │ └─ todo.py # Todo 相關路由
│ ├─ services/
│ │ ├─ __init__.py
│ │ └─ todo_service.py # 商業邏輯
│ └─ models/
│ ├─ __init__.py
│ ├─ todo.py # Pydantic + ORM Model
│ └─ base.py # 共用 BaseModel
│
├─ tests/
│ └─ test_todo.py
│
└─ requirements.txt
重點:所有子目錄都放置
__init__.py,讓 Python 把它們視為套件,方便from app.routers import todo這類的匯入。
3. 路由 (router)
FastAPI 的 APIRouter 讓我們把不同資源的路由分散到不同檔案,最後在 main.py 中統一掛載。
範例:app/routers/todo.py
# app/routers/todo.py
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from app.models.todo import TodoCreate, TodoRead, TodoUpdate
from app.services.todo_service import TodoService
router = APIRouter(prefix="/todos", tags=["Todo"])
# 依賴注入 Service,保持路由層的輕量
def get_service() -> TodoService:
return TodoService()
@router.post("/", response_model=TodoRead, status_code=status.HTTP_201_CREATED)
async def create_todo(
todo_in: TodoCreate,
service: TodoService = Depends(get_service)
):
"""建立一筆 Todo"""
return await service.create(todo_in)
@router.get("/", response_model=List[TodoRead])
async def list_todos(
service: TodoService = Depends(get_service)
):
"""取得全部 Todo 列表"""
return await service.list_all()
@router.put("/{todo_id}", response_model=TodoRead)
async def update_todo(
todo_id: int,
todo_in: TodoUpdate,
service: TodoService = Depends(get_service)
):
"""更新指定的 Todo"""
updated = await service.update(todo_id, todo_in)
if not updated:
raise HTTPException(status_code=404, detail="Todo not found")
return updated
router = APIRouter(... ):設定路徑前綴與分組標籤。- 依賴注入 (
Depends):將 Service 注入,使路由層不必自行建立實例,方便測試與未來的 DI 容器擴充。
4. 服務層 (service)
服務層負責 業務邏輯、資料庫存取、交易控制等,保持路由層的「薄」與「純粹」。
範例:app/services/todo_service.py
# app/services/todo_service.py
from typing import List, Optional
from app.models.todo import TodoCreate, TodoRead, TodoUpdate, TodoORM
from app.db import get_async_session # 假設已有 async DB session 工具
class TodoService:
"""Todo 相關的商業邏輯"""
async def create(self, data: TodoCreate) -> TodoRead:
async with get_async_session() as session:
todo = TodoORM(**data.dict())
session.add(todo)
await session.commit()
await session.refresh(todo)
return TodoRead.from_orm(todo)
async def list_all(self) -> List[TodoRead]:
async with get_async_session() as session:
result = await session.execute(
TodoORM.select()
)
todos = result.scalars().all()
return [TodoRead.from_orm(t) for t in todos]
async def update(self, todo_id: int, data: TodoUpdate) -> Optional[TodoRead]:
async with get_async_session() as session:
todo = await session.get(TodoORM, todo_id)
if not todo:
return None
for field, value in data.dict(exclude_unset=True).items():
setattr(todo, field, value)
await session.commit()
await session.refresh(todo)
return TodoRead.from_orm(todo)
async with get_async_session():示範如何在服務層統一管理 DB session,避免在路由層出現session相關程式碼。- Pydantic 與 ORM 之間的轉換:使用
TodoRead.from_orm()把 ORM 物件轉成回傳的 Pydantic 模型。
5. 資料模型 (models)
模型層主要有兩種:
- Pydantic Model(用於請求/回應驗證)
- ORM Model(與資料庫對應)
範例:app/models/todo.py
# app/models/todo.py
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
# ---------- Pydantic Schemas ----------
class TodoBase(BaseModel):
title: str = Field(..., max_length=100, description="Todo 標題")
description: Optional[str] = Field(None, max_length=500)
class TodoCreate(TodoBase):
"""建立 Todo 時使用的 Schema"""
pass
class TodoUpdate(BaseModel):
"""更新 Todo 時使用的 Schema,允許部分欄位更新"""
title: Optional[str] = Field(None, max_length=100)
description: Optional[str] = Field(None, max_length=500)
class TodoRead(TodoBase):
"""回傳給前端的 Schema"""
id: int
created_at: datetime
class Config:
orm_mode = True # 讓 Pydantic 支援 ORM 物件
# ---------- SQLAlchemy ORM ----------
from sqlalchemy import Column, Integer, String, Text, DateTime, func
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class TodoORM(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
orm_mode = True:讓 Pydantic 能直接接受 ORM 物件。- 分離
TodoBase、TodoCreate、TodoUpdate:避免在 更新 時必須填滿所有欄位,提升 API 使用彈性。
6. 入口檔案 main.py
最後在 main.py 中把所有 router 匯入並啟動 FastAPI 應用。
# app/main.py
from fastapi import FastAPI
from app.routers import todo
app = FastAPI(
title="Todo API",
description="示範 FastAPI 多檔案拆分的範例專案",
version="0.1.0",
)
# 將子 router 加入主應用
app.include_router(todo.router)
# 若有其他 router (e.g., users, auth) 只需要再 import 並 include
# app.include_router(user.router)
執行:
uvicorn app.main:app --reload
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 解決方式 / 最佳實踐 |
|---|---|---|
| 循環匯入 (Circular import) | Router 直接匯入 Service,Service 又匯入 Router 內的型別或 DB 工具。 | 使用依賴注入 (Depends) 把 Service 交給 FastAPI 管理,或把共用類別搬到 models / schemas 中。 |
| 全域 DB Session | 在 router 中直接使用 SessionLocal(),導致多個請求共享同一個連線。 |
使用 context manager (async with get_async_session()) 或 FastAPI 的 依賴式 Depends(get_db),確保每個請求都有獨立 session。 |
| 路由太肥 | 把大量驗證、商業邏輯寫在路由函式內。 | 保持路由薄:僅負責參數驗證與回傳結果,所有實際處理交給 Service。 |
| 模型與 ORM 混用 | 把 ORM 類別直接當作 response_model,會失去 Pydantic 的驗證與自動文件化。 | 分離:使用 TodoRead (Pydantic) 作為 response_model,在 Service 中 TodoRead.from_orm() 轉換。 |
| 硬編碼路徑 | 在 include_router 時寫死字串路徑,後續改動麻煩。 |
使用相對匯入 (from app.routers import todo) 並在 router 中設定 prefix,統一管理。 |
最佳實踐總結:
- 保持每層職責單一:Router → Service → Model。
- 使用 Pydantic 的
orm_mode讓資料在 ORM ↔︎ Pydantic 之間自動轉換。 - 依賴注入 (
Depends) 是 FastAPI 的核心機制,務必在 router 與 DB session 中使用。 - 測試友好:在 Service 中寫純 Python 函式,測試時只需要 mock DB session;Router 測試則使用
TestClient。 - 加入型別提示:所有函式都加上
-> TodoRead、-> List[TodoRead],IDE 會自動補全,降低錯誤率。
實際應用場景
1. 企業內部的待辦清單系統
- 需求:多部門共用同一個待辦清單,需支援 CRUD、搜尋、權限驗證。
- 實作:
routers/todo.py處理 HTTP 請求。services/todo_service.py內加入 部門過濾、權限檢查(例如if user.department not in todo.allowed_departments)。models/todo.py加入department_id欄位與關聯表。
2. 電子商務平台的商品管理
- 需求:商品 CRUD、批次上架、價格計算。
- 實作:
routers/product.py、services/product_service.py、models/product.py同樣遵循三層分離。- 服務層 中加入 交易 (transaction),確保批次上架失敗時回滾。
- 模型層 使用 SQLAlchemy mixins 讓所有表都有
created_at、updated_at。
3. 微服務間的共用套件
- 需求:多個微服務共用 User、Token 的驗證模型。
- 實作:
- 把共用的 Pydantic schema 放在
app/models/common.py,其他服務只要from app.models.common import TokenPayload即可。 - 這樣即使某個微服務需要新增欄位,也只要在同一個檔案更新即可,保持一致性。
- 把共用的 Pydantic schema 放在
總結
將 FastAPI 應用拆分成 router、service、model 三層結構,是提升可維護性、可測試性與團隊協作效率的關鍵。
透過 APIRouter 讓路由模組化、在服務層集中商業邏輯、使用 Pydantic + SQLAlchemy 完成資料驗證與 ORM 映射,我們可以:
- 快速擴充:新增功能只需新增對應的 router、service、model 檔案。
- 降低耦合:各層只透過明確的介面溝通,避免循環匯入。
- 提升測試效率:服務層與模型層可獨立單元測試,路由層則使用
TestClient進行整合測試。
掌握上述概念後,你的 FastAPI 專案將能在 開發速度、程式品質 與 維護成本 三方面取得最佳平衡。祝開發順利,期待看到你用這套結構打造出更大型、更穩定的 API!