搭建一个清晰的 FastAPI 项目骨架
当你的应用逐渐发展壮大,业务逻辑变得愈发复杂,模块间需要更细粒度的解耦与协作时,你可能会发现当前这种结构会略显不足。 此时,我们便会更推荐采用一种更具扩展性的三层架构——路由层(Router)、服务层(Service)和数据仓库层(Repository):
- 路由层 (Router):负责接收HTTP请求,进行初步的参数验证,并协调调用服务层的方法。它专注于API接口的定义和请求-响应的处理,保持轻量。
- 服务层 (Service):承载核心业务逻辑。它将调用数据仓库层来执行数据库操作,并在此基础上处理复杂的业务规则、数据转换等,保持与HTTP层和数据库层的解耦。
- 数据仓库层 (Repository):专注于与数据库的交互,封装所有的CRUD(创建、读取、更新、删除)操作,对外提供统一的数据访问接口,使得业务逻辑层无需关心具体的ORM细节,例如本示例中的crud模块就可以演变为repository层的一部分。
.
├── app/
│ ├── __init__.py # 初始化模块
│ ├── main.py # 应用入口文件
│ ├── core/ # 核心配置和工具模块
│ │ ├── __init__.py
│ │ ├── config.py # 配置文件,如数据库连接、JWT 密钥等
│ │ ├── security.py # 安全相关工具(如密码加密、JWT 生成与验证)
│ │ └── dependencies.py # 全局依赖项
│ ├── models/ # 数据库模型
│ │ ├── __init__.py
│ │ ├── base.py # 基础模型和数据库连接
│ │ └── user.py # 用户模型
│ ├── schemas/ # 数据验证和序列化
│ │ ├── __init__.py
│ │ ├── user.py # 用户相关 Pydantic 模型
│ ├── crud/ # 数据库操作(CRUD)
│ │ ├── __init__.py
│ │ └── user.py # 用户相关 CRUD 操作
│ ├── api/ # API 路由
│ │ ├── __init__.py
│ │ ├── deps.py # API 路由依赖项
│ │ ├── user.py # 用户相关路由
│ │ └── auth.py # 身份验证路由
│ ├── tests/ # 测试用例
│ │ ├── __init__.py
│ │ ├── test_auth.py # 身份验证相关测试
│ │ └── test_user.py # 用户相关测试
├── .env # 环境变量
├── requirements.txt # 项目依赖
└── alembic/ # 数据库迁移(可选)
├── env.py
└── versions/
main.py
from fastapi import FastAPI
from app.api import auth, user
app = FastAPI()
# 注册路由
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
app.include_router(user.router, prefix="/users", tags=["Users"])
core/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
JWT_SECRET: str = "your_jwt_secret"
JWT_ALGORITHM: str = "HS256"
DATABASE_URL: str = "sqlite+aiosqlite:///./test.db"
class Config:
env_file = ".env"
settings = Settings()
core/security.py
from passlib.context import CryptContext
from jose import jwt
from datetime import datetime, timedelta
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: timedelta = timedelta(minutes=30)):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
models/base.py
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from app.core.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
models/user.py
from sqlalchemy import String, Integer, Boolean
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
email: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
hashed_password: Mapped[str] = mapped_column(String, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
schemas/user.py
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserResponse(BaseModel):
id: int
email: EmailStr
is_active: bool
model_config = ConfigDict(from_attributes=True)
crud/user.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.models.user import User
from app.schemas.user import UserCreate
from app.core.security import hash_password
async def get_user_by_email(db: AsyncSession, email: str) -> User | None:
result = await db.execute(select(User).filter(User.email == email))
return result.scalars().first()
async def create_user(db: AsyncSession, user: UserCreate) -> User:
db_user = User(email=user.email, hashed_password=hash_password(user.password))
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user
api/auth.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.user import UserCreate
from app.core.security import verify_password, create_access_token
from app.crud.user import get_user_by_email
from app.core.dependencies import get_db
router = APIRouter()
@router.post("/login")
async def login(email: str, password: str, db: AsyncSession = Depends(get_db)):
user = await get_user_by_email(db, email)
if not user or not verify_password(password, user.hashed_password):
raise HTTPException(status_code=400, detail="Invalid credentials")
token = create_access_token(data={"sub": user.email})
return {"access_token": token, "token_type": "bearer"}
core/dependencies.py
from app.models.base import async_session
async def get_db():
async with async_session() as session:
yield session