27. FastAPI 业务划分(模块化)
按业务划分是为了让代码有归属
项目变大后,用户、商品、订单都有自己的接口、模型和规则。模块化先回答“这段代码属于哪种业务”,而不是先把项目拆成几十层。
小项目可以每个业务一个路由文件;复杂项目再按业务包组织 router.py、schemas.py、service.py。相关修改尽量集中,跨业务交互通过清晰的函数或服务接口完成。
最小可运行的两个业务模块
project/
├── main.py
└── routers/
├── __init__.py
├── users.py
└── products.py创建 routers/users.py:
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["用户"])
@router.get("/{user_id}")
def detail(user_id: int):
return {"id": user_id, "name": "用户示例"}创建 routers/products.py:
from fastapi import APIRouter
router = APIRouter(prefix="/products", tags=["商品"])
@router.get("")
def list_products():
return [{"id": 1, "name": "键盘"}]创建空 routers/__init__.py,然后在项目根目录创建 main.py:
from fastapi import FastAPI
from routers import products, users
app = FastAPI()
app.include_router(users.router, prefix="/api/v1")
app.include_router(products.router, prefix="/api/v1")在 project/ 目录执行 uv run uvicorn main:app --reload。如果尚未安装依赖,先执行 uv add fastapi "uvicorn[standard]"。
启动后可以访问:
| 地址 | 作用 |
|---|---|
http://127.0.0.1:8000/api/v1/users/1 | 查询用户示例 |
http://127.0.0.1:8000/api/v1/products | 查询商品列表 |
http://127.0.0.1:8000/docs | 查看按业务标签分组的交互式接口文档 |
main:app 中的 main 是模块名,对应 main.py;app 是这个文件中的 FastAPI 实例变量名,不是随便填写的字符串。
routers/init.py 到底有什么作用
可以把 routers/ 理解成装路由的文件夹,把 __init__.py 理解成这个包的入口。
它主要有两个作用:
- 把目录声明为普通 Python 包,让 Python 按包的方式组织其中的模块。
- 提供包级初始化和统一导出的地方。其他文件导入
routers时,可以通过这个入口获取你选择暴露的对象。
这里要区分 Python 的“导入”和 FastAPI 的“注册”:__init__.py 属于 Python 的包机制,它不会自动扫描所有路由文件,也不会自动把接口注册到 FastAPI 应用。
没有 init.py 就一定不能导入吗?
不一定。现代 Python 支持没有 __init__.py 的命名空间包,但它主要用于更特殊的包组织场景。本章使用普通包,建议明确创建 __init__.py,让目录意图更清晰。
写法一:留空,main.py 分别注册业务模块
对于本章这样的入门项目,routers/__init__.py 留空即可。也可以只写一行说明性注释:
# routers/__init__.py
# 路由包入口,暂时不需要初始化逻辑。对应的完整 main.py:
from fastapi import FastAPI
from routers import products, users
app = FastAPI()
app.include_router(users.router, prefix="/api/v1")
app.include_router(products.router, prefix="/api/v1")为什么入口文件没有写导入,from routers import users 仍然能用?
因为这里导入的是 routers 包中的 users 子模块,Python 可以找到并加载 routers/users.py,不需要你先在 __init__.py 中把它导入一遍。
这几行代码可以按下面的顺序理解:
| 代码 | 含义 |
|---|---|
from routers import products, users | 加载两个业务模块,执行其中创建路由和定义接口的代码 |
app = FastAPI() | 创建整个应用的入口实例 |
users.router | 取出 users.py 中定义的 APIRouter 对象 |
app.include_router(...) | 把该路由对象中的接口注册到应用,并加上应用级前缀 |
注意:users 是模块,users.router 才是路由对象,所以不能写成 app.include_router(users)。
写法二:在 init.py 中统一导出路由对象
当希望 main.py 直接使用路由对象,而不关心业务模块叫什么时,可以在入口文件中给路由起明确的名字。
routers/__init__.py:
from .products import router as products_router
from .users import router as users_router这里的 . 表示当前包,也就是 routers。from .users import router 表示从同一个包内的 users.py 导入 router。
两个业务文件都把路由命名为 router,所以统一导出时用 as 起别名。如果都直接写 from .xxx import router,后导入的对象会覆盖前一个同名变量。
对应的完整 main.py:
from fastapi import FastAPI
from routers import products_router, users_router
app = FastAPI()
app.include_router(users_router, prefix="/api/v1")
app.include_router(products_router, prefix="/api/v1")现在 users_router 已经是 APIRouter 对象,不需要再写 users_router.router。
有些项目还会在 __init__.py 中添加:
__all__ = ["users_router", "products_router"]__all__ 主要控制 from routers import * 时导入哪些名字,也能表达包希望公开的接口。上面的显式导入并不依赖它;它既不是权限控制,也不负责注册 FastAPI 路由。入门项目可以不写,并尽量避免使用 import *。
写法三:在 init.py 中汇总成一个总路由
业务模块更多时,可以让 routers 包向外只暴露一个总路由,让 main.py 只注册一次。
保留前面的 users.py 和 products.py 不变,将 routers/__init__.py 改为:
from fastapi import APIRouter
from .products import router as products_router
from .users import router as users_router
router = APIRouter()
router.include_router(users_router)
router.include_router(products_router)这里新建的 router 是一个总路由,先把用户路由和商品路由收集起来。它仍然不是 FastAPI 应用,也不会单独启动服务器。
对应的完整 main.py:
from fastapi import FastAPI
from routers import router as api_router
app = FastAPI()
app.include_router(api_router, prefix="/api/v1")用户接口的组合过程是:
users.py:定义 /users/{user_id}
↓
routers/__init__.py:把用户路由加入总路由
↓
main.py:把总路由加入 app,并加上 /api/v1
↓
最终接口:/api/v1/users/{user_id}三种写法的最终接口地址相同,差别只是导入与注册代码放在哪里:
| 方案 | init.py 写什么 | main.py 负责什么 | 适用情况 |
|---|---|---|---|
| 留空 | 不写代码,或只写注释 | 导入各业务模块,分别注册 | 初学或业务模块较少 |
| 统一导出 | 导入路由对象,并起别名 | 导入各路由对象,分别注册 | 希望统一包的对外名字 |
| 总路由 | 创建总路由,收集各业务路由 | 导入并注册总路由 | 业务模块较多,集中维护注册列表 |
选择其中一套配对写法即可。使用总路由后,不要又在 main.py 中把 users.router、products.router 注册一遍,否则会重复注册相同接口。
init.py 里面不适合写什么
导入一个包时,Python 会执行它的 __init__.py,通常在同一个解释器进程中首次导入时执行。因此这里适合放轻量的导出和路由汇总代码,不适合放耗时操作或重要业务流程。
不要在这里直接连接数据库、发送网络请求、执行下单逻辑,或者启动 Uvicorn。需要随应用启动和关闭管理的资源,应使用 FastAPI 的 lifespan 机制,而不是依赖导入时的副作用。
还要注意循环导入:main.py 导入 routers,routers/__init__.py 导入 users.py,如果 users.py 又写 from main import app,就可能在模块尚未初始化完成时再次访问它。
本章的依赖方向应保持为:
main.py → routers 包 → users.py / products.py业务路由使用自己的 APIRouter,不要反过来导入 main.py 中的 app。共享配置和依赖应放在独立模块中。
前缀是逐层拼起来的
最终 URL = include_router 的前缀 + APIRouter 的前缀 + 操作路径。不要在所有位置都重复写 /users,否则容易得到 /users/users/...。标签只影响文档组织,不提供权限隔离。
APIRouter 是路由集合,不是独立启动的服务器;只有导入文件还不够,必须 include_router 注册。一个业务包也可以向外只暴露 router,减少其他模块依赖内部细节。
模块边界怎么判断
用户模块处理账户资料,订单模块处理下单和状态流转。订单需要用户信息时调用用户服务,而不是直接调用用户的 HTTP 路由函数。共享配置、数据库连接和错误定义可以放公共基础设施,业务规则不要全部堆到 utils.py。
模块化不意味着微服务。单个应用内的业务包通常已经足够;跨进程拆分会带来网络失败、分布式事务和部署成本,需要真实需求支撑。
练习:加入订单路由模块,注册 /api/v1/orders;故意漏掉 include_router,观察为何文件存在但接口 404。