系列:Python Web 框架
项目结构这件事很容易走两个极端。
一种是所有代码都塞在 main.py、views.py 或 app.py 里,写到几千行才发现不好改。另一种是一上来就搞 domain/、application/、infrastructure/,业务还没开始,目录先把人劝退。
我更喜欢按项目阶段拆。小项目就小结构,变复杂了再加层。结构的目的不是显得高级,而是让代码变化时少牵连。
这篇会横跨 Django、FastAPI、Flask。具体框架入门可以先看 Django 安装与使用指南、FastAPI 安装与使用指南 和 Flask 安装与使用指南。
先别急着拆
一个只有 5 个接口的小项目,用这种结构完全够:
app/├── main.py├── config.py└── tests/ └── test_main.py问题不在“文件少”,而在“职责混在一起”。如果 main.py 里同时有路由、SQL、第三方 API、密码加密、日志配置,那就该拆。
我通常用三个信号判断是否需要拆:
单个文件超过 300 行,且还在增长测试某个函数需要启动整个 Web 应用一个需求改动要同时碰很多无关代码满足其中两个,就可以动手。
通用分层:HTTP 层不要写业务
无论框架怎么选,一个原则都通用:
路由层处理 HTTP服务层处理业务数据层处理存取配置层处理环境差异比如 FastAPI 里不要这样写:
@app.post("/orders")def create_order(data: OrderCreate, session: Session = Depends(get_session)): user = session.get(User, data.user_id) if not user: raise HTTPException(status_code=404) order = Order(user_id=user.id, amount=data.amount) session.add(order) session.commit() send_message(user.email, "order created") return order这段代码能跑,但测试和复用都难。
更好的方向:
@router.post("/orders")def create_order(data: OrderCreate, session: Session = Depends(get_session)): return order_service.create_order(session, data)业务放到 order_service.py:
def create_order(session, data): user = get_user_or_raise(session, data.user_id) order = save_order(session, user.id, data.amount) notify_order_created(user.email) return order路由层薄一点,后面换 CLI、定时任务、测试入口都更轻松。
FastAPI 推荐结构
中小型 FastAPI 项目可以这样:
app/├── main.py├── config.py├── database.py├── dependencies.py├── routers/│ ├── users.py│ └── orders.py├── schemas/│ ├── users.py│ └── orders.py├── services/│ ├── users.py│ └── orders.py└── repositories/ ├── users.py └── orders.pytests/├── test_users.py└── test_orders.py不是每个目录都必须一开始就有。我的顺序通常是:
先拆 routers再拆 schemas业务变复杂后拆 services数据库查询重复后拆 repositoriesmain.py 应该只做组装:
from fastapi import FastAPIfrom app.routers import orders, users
app = FastAPI()app.include_router(users.router, prefix="/users", tags=["users"])app.include_router(orders.router, prefix="/orders", tags=["orders"])结构再漂亮,也还是要靠测试和运行检查兜底。目录不能替你保证质量。
我故意放这个小错误,是想强调一点:结构再漂亮,还是要靠测试和运行检查兜底。目录不能替你保证质量。
Django 推荐结构
Django 不太适合按 services/、repositories/ 从根目录硬拆。它本来就是 app 组织方式。
一个常见结构:
mysite/├── settings.py├── urls.py└── asgi.pyaccounts/├── models.py├── views.py├── urls.py├── forms.py├── selectors.py├── services.py└── tests.pyorders/├── models.py├── views.py├── urls.py├── selectors.py├── services.py└── tests.py这里两个名字很好用:
selectors.py:只读查询services.py:会修改状态的业务动作比如:
def get_user_orders(user): return Order.objects.filter(user=user).order_by("-created_at")def cancel_order(order, user): if order.user_id != user.id: raise PermissionError("cannot cancel this order") order.status = Order.Status.CANCELED order.save(update_fields=["status"]) return orderDjango 的 ORM 已经很强,没必要为了“分层”再套一层无意义 repository。除非你真的要隔离存储实现。
Flask 推荐结构
Flask 小项目可以从 app.py 开始,但一旦有多个模块,建议改成应用工厂:
app/├── __init__.py├── config.py├── extensions.py├── blueprints/│ ├── users.py│ └── orders.py├── services/│ └── orders.py└── templates/wsgi.pytests/app/__init__.py:
from flask import Flask
from .blueprints.users import bp as users_bpfrom .blueprints.orders import bp as orders_bp
def create_app(): app = Flask(__name__) app.config.from_object("app.config.Config") app.register_blueprint(users_bp, url_prefix="/users") app.register_blueprint(orders_bp, url_prefix="/orders") return appwsgi.py:
from app import create_app
app = create_app()Flask 的结构没有 Django 那么强约束,所以更要克制。不要每写一个函数就新建一层目录。
配置怎么拆
配置不要散落在代码里。
比较通用的方式:
from dataclasses import dataclassimport os
@dataclass(frozen=True)class Settings: database_url: str = os.environ["DATABASE_URL"] secret_key: str = os.environ["SECRET_KEY"] debug: bool = os.environ.get("DEBUG") == "1"
settings = Settings()如果是 Django,可以拆多个 settings 文件:
mysite/settings/├── base.py├── local.py└── production.py但也不要过度拆。很多小项目一个 settings.py 加环境变量已经够了。
核心原则:
密钥不进 Git默认值只给非敏感配置生产配置显式传入测试环境能单独覆盖配置测试目录放哪
两种方式都可以。
放在 app 内:
orders/└── tests/ ├── test_models.py └── test_services.py放在根目录:
tests/├── test_orders.py└── test_users.py我更喜欢根目录 tests/,因为很多测试会跨模块。Django 大项目也可以按 app 放,配合 pytest-django 一样好用。
测试重点不要只测路由。服务层函数更值得测:
def test_cancel_order_changes_status(order, user): result = cancel_order(order, user)
assert result.status == Order.Status.CANCELED路由测试负责确认 HTTP 状态码和响应结构,业务规则尽量在普通函数测试里覆盖。
常见问题
要不要上 DDD
大多数 Python Web 项目不需要一开始就 DDD。
如果业务非常复杂,领域对象生命周期长,团队也理解 DDD,可以引入一部分概念。但为了一个 CRUD 后台强行建 domain/、use_cases/、adapters/,通常会让新人更难改代码。
先把 HTTP、业务、数据访问分开,就已经够用了。
service 和 repository 怎么分
简单说:
service:业务动作repository:数据存取但 Django 项目里 repository 经常没必要,因为 ORM 查询表达力足够强。FastAPI + SQLAlchemy 项目里,如果查询重复很多,repository 会更有价值。
不要为了“每张表一个 repository”而拆。等重复出现再拆。
配置类要不要用 Pydantic
FastAPI 项目可以用 Pydantic Settings,类型清晰,也适合从环境变量读取。
Django/Flask 项目用普通 Python 配置也没问题。工具不是重点,重点是配置来源清楚、敏感信息不进仓库。
什么时候拆微服务
比你想象得晚。
先把单体内部边界拆清楚。如果一个模块已经有独立数据、独立发布节奏、独立团队维护,再考虑拆服务。否则微服务只会把函数调用变成网络调用,把本地事务变成分布式问题。
总结
项目结构要服务于变化,而不是服务于审美。
我自己的默认路径是:
小项目:少文件,先跑通路由变多:拆 routers / blueprints / app urls业务变复杂:拆 services查询重复:拆 selectors 或 repositories配置混乱:集中配置和环境变量测试困难:把业务从 HTTP 层拿出来Django、FastAPI、Flask 的目录长得不一样,但底层原则一样:HTTP 层薄一点,业务逻辑集中一点,配置和密钥清楚一点,测试能直接打到核心函数。
后续如果要上线,再接着看 Python Web 项目部署指南:Gunicorn、Uvicorn、Nginx 和 Docker。项目结构和部署结构是连在一起的,目录清楚,部署脚本也会简单很多。