Skip to content

27. FastAPI 业务划分(模块化) ​

按业务划分是为了让代码有归属 ​

项目变大后,用户、商品、订单都有自己的接口、模型和规则。模块化先回答“这段代码属于哪种业务”,而不是先把项目拆成几十层。

小项目可以每个业务一个路由文件;复杂项目再按业务包组织 router.py、schemas.py、service.py。相关修改尽量集中,跨业务交互通过清晰的函数或服务接口完成。

最小可运行的两个业务模块 ​

text
project/
├── main.py
└── routers/
    ├── __init__.py
    ├── users.py
    └── products.py

创建 routers/users.py:

python
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:

python
from fastapi import APIRouter

router = APIRouter(prefix="/products", tags=["商品"])

@router.get("")
def list_products():
    return [{"id": 1, "name": "键盘"}]

创建空 routers/__init__.py,然后在项目根目录创建 main.py:

python
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 理解成这个包的入口。

它主要有两个作用:

  1. 把目录声明为普通 Python 包,让 Python 按包的方式组织其中的模块。
  2. 提供包级初始化和统一导出的地方。其他文件导入 routers 时,可以通过这个入口获取你选择暴露的对象。

这里要区分 Python 的“导入”和 FastAPI 的“注册”:__init__.py 属于 Python 的包机制,它不会自动扫描所有路由文件,也不会自动把接口注册到 FastAPI 应用。

没有 init.py 就一定不能导入吗?

不一定。现代 Python 支持没有 __init__.py 的命名空间包,但它主要用于更特殊的包组织场景。本章使用普通包,建议明确创建 __init__.py,让目录意图更清晰。

写法一:留空,main.py 分别注册业务模块 ​

对于本章这样的入门项目,routers/__init__.py 留空即可。也可以只写一行说明性注释:

python
# routers/__init__.py
# 路由包入口,暂时不需要初始化逻辑。

对应的完整 main.py:

python
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:

python
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:

python
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 中添加:

python
__all__ = ["users_router", "products_router"]

__all__ 主要控制 from routers import * 时导入哪些名字,也能表达包希望公开的接口。上面的显式导入并不依赖它;它既不是权限控制,也不负责注册 FastAPI 路由。入门项目可以不写,并尽量避免使用 import *。

写法三:在 init.py 中汇总成一个总路由 ​

业务模块更多时,可以让 routers 包向外只暴露一个总路由,让 main.py 只注册一次。

保留前面的 users.py 和 products.py 不变,将 routers/__init__.py 改为:

python
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:

python
from fastapi import FastAPI
from routers import router as api_router

app = FastAPI()

app.include_router(api_router, prefix="/api/v1")

用户接口的组合过程是:

text
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,就可能在模块尚未初始化完成时再次访问它。

本章的依赖方向应保持为:

text
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。