首页
/ FastAPI 子应用挂载(Mounts):让多个独立 FastAPI 应用共享一个服务端口

FastAPI 子应用挂载(Mounts):让多个独立 FastAPI 应用共享一个服务端口

2026-09-04 14:09:25作者:廉皓灿Ida

在 FastAPI 中,当你需要运行两个(或更多)完全独立的应用——各自拥有独立的 OpenAPI Schema 和独立的 Swagger 文档界面,却又希望它们部署在同一个端口、同一个服务器进程下时,可以使用子应用挂载(Mount)机制:创建一个主应用(Top-Level Application),然后把一个或多个子应用"挂载"(mount)到指定路径下。本文基于 FastAPI 官方文档 Sub Applications – Mounts 展开,并结合 fastapi/applications.py 的源码与教程测试用例,讲解挂载的完整操作步骤、验证方法,以及底层 root_path 机制的工作原理。

读完本文后,你将能够:

  • app.mount() 把独立 FastAPI 应用挂载到主应用的指定路径;
  • 理解主应用与子应用各自的 /docs/redoc/openapi.json 端点如何自动生成且互不干扰;
  • 从源码层面理解 ASGI root_path 如何自动传递挂载前缀,使子应用的文档 UI 与 OpenAPI servers 字段正确工作。

主应用 FastAPI Swagger UI 只展示其自身的 /app 端点

挂载到 /subapi 的子应用 Swagger UI,所有路径带 /subapi 前缀

什么是"挂载"(Mounting)

"挂载"(Mounting)是指在一个特定路径下添加一个完全独立的应用程序。挂载完成后,该路径下的所有请求都会被交给这个子应用,由其内部声明的 路径操作(path operations)来处理。

这与在单个应用内使用 APIRouter + prefix 不同:挂载上去的是一个完整的 FastAPI 实例,它有自己独立的:

  • OpenAPI Schema(/openapi.json);
  • 文档界面(/docs/redoc);
  • 异常处理器、依赖覆盖、中间件等应用级配置。

主应用与子应用之间不共享路由表,互不可见对方的路径操作——这正是"两个独立应用"语义的关键。

完整示例:主应用 + 子应用

官方教程的完整可运行代码位于 tutorial001_py310.py,整个文件只有 19 行:

from fastapi import FastAPI

app = FastAPI()


@app.get("/app")
def read_main():
    return {"message": "Hello World from main app"}


subapi = FastAPI()


@subapi.get("/sub")
def read_sub():
    return {"message": "Hello World from sub API"}


app.mount("/subapi", subapi)

下面按官方文档的三个步骤拆解这段代码。

第一步:创建顶层(Top-Level)应用

首先创建主应用 app 及其路径操作:

app = FastAPI()


@app.get("/app")
def read_main():
    return {"message": "Hello World from main app"}

这里 app 是最终由 ASGI 服务器(如 Uvicorn)启动的入口应用,它声明了一个 GET /app 端点。

第二步:创建子应用

然后创建子应用 subapi 及其路径操作:

subapi = FastAPI()


@subapi.get("/sub")
def read_sub():
    return {"message": "Hello World from sub API"}

注意:subapi 只是一个标准的 FastAPI 应用,与任何普通应用没有区别,区别仅在于它会被"挂载"到主应用上,而不是直接作为服务入口。

第三步:把子应用挂载到主应用

在顶层应用 app 上调用 mount(),把 subapi 挂载到路径 /subapi

app.mount("/subapi", subapi)

从此,所有以 /subapi 开头的请求都会进入 subapi,由它自己的路由表处理(例如 /subapi/sub 会命中 read_sub 函数)。

mount() 方法继承自 Starlette(FastAPI(Starlette) 定义见 applications.py)。从源码结构看,Starlette 的 mount 实现只是委托给路由器:

def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None:
    self.router.mount(path, app=app, name=name)

即在 ASGI 路由层面把整个子应用作为一个"路由目标"注册到指定前缀下。

运行并验证自动生成的 API 文档

使用 uv 运行 FastAPI CLI 开发服务器(代码文件需在项目根目录下):

$ uv run fastapi dev

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

主应用的文档:http://127.0.0.1:8000/docs

打开 http://127.0.0.1:8000/docs,你会看到主应用的自动 API 文档,其中只显示主应用自己的路径操作(/app),完全看不到子应用的内容。

子应用的文档:http://127.0.0.1:8000/subapi/docs

再打开 http://127.0.0.1:8000/subapi/docs,你会看到子应用独立的 Swagger UI,它同样只包含子应用自己的路径操作,但所有路径都带上了正确的前缀 /subapi(即 GET /subapi/sub)。

如果分别在这两个文档界面中尝试 "Try it out",两者都能正常工作——因为浏览器会直接与各自对应的那个应用(或子应用)通信。同理,两个应用的 openapi.json 也是相互独立的:

  • 主应用:http://127.0.0.1:8000/openapi.json
  • 子应用:http://127.0.0.1:8000/subapi/openapi.json

仓库中的测试用例 test_tutorial001.py 精确验证了上述行为:

  • test_main 断言 GET /app 返回 {"message": "Hello World from main app"}
  • test_sub 断言 GET /subapi/sub 返回 {"message": "Hello World from sub API"}
  • test_openapi_schema_main 断言主应用的 /openapi.jsonpaths 只包含 /app,且没有 servers 字段;
  • test_openapi_schema_sub 断言 /subapi/openapi.jsonpaths 只包含 /sub,并且额外带有 "servers": [{"url": "/subapi"}]

servers 字段是 OpenAPI 3 规范中的服务器地址声明——测试快照证实了子应用的 Schema 自动注入了挂载前缀,这让任何遵循 OpenAPI 规范的客户端工具都能把请求正确发往 /subapi 下的地址。

技术细节:root_path 机制

这是子应用挂载能"开箱即用"的核心。当你按上述方式挂载子应用时,FastAPI 会借助 ASGI 规范中的 root_path 机制,自动把挂载路径传递给子应用,子应用据此知道自己的文档 UI 应该使用哪个路径前缀。

从源码可以确认这一链路的几个关键位置(均在 applications.py 中):

1. ASGI 启动时写入 root_path 应用作为 ASGI 可调用对象被调用时,如果显式配置了 root_path(见下文构造函数参数),会写入请求 scope

if self.root_path:
    scope["root_path"] = self.root_path

applications.py)当应用是被上层 Starlette 路由器以 Mount 方式挂载时,则由 ASGI 服务器/上层路由在把请求分发给子应用之前设置 scope["root_path"] 为挂载前缀。

2. 动态注入 servers 字段。get_openapi 生成 Schema 的入口处,FastAPI 检查当前请求的 root_path,若非空则把它加到 Schema 的 servers 列表最前面:

root_path = req.scope.get("root_path", "").rstrip("/")
if root_path and self.root_path_in_servers:
    server_urls = {s.get("url") for s in schema.get("servers", [])}
    if root_path not in server_urls:
        schema["servers"] = [{"url": root_path}] + schema.get("servers", [])

applications.py)这正是测试中子应用 Schema 出现 "servers": [{"url": "/subapi"}] 的原因。

3. 文档 URL 本身也带上前缀。 子应用的 /docs/openapi.json 端点在响应前也会读取 scope["root_path"] 并拼接前缀:

root_path = req.scope.get("root_path", "").rstrip("/")
openapi_url = root_path + self.openapi_url
oauth2_redirect_url = root_path + oauth2_redirect_url

applications.py)因此 Swagger UI 中发起请求的 base URL 会正确指向 /subapi/openapi.json,"Try it out" 才能打到子应用。

相关构造函数参数

FastAPI() 构造函数(applications.py)提供了两个与挂载/前缀直接相关的参数,值得在子应用场景下了解:

参数 默认值 作用
root_path "" 显式声明应用部署的路径前缀,写入 scope["root_path"],用于生成 OpenAPI servers 及文档 URL。文档提示 openapi_prefix 已被弃用并改用 root_path(见 applications.py)。
root_path_in_servers True 关闭后,不会自动用 root_path 生成 OpenAPI 的 servers 字段(见 applications.py)。
servers [] 手动指定 OpenAPI servers 列表;当其为空时,FastAPI 会依据 root_path 自动填充(见 applications.py 的参数文档)。

对于通过 mount() 挂载的子应用,通常不需要手动设置 root_path——ASGI 分发过程会自动完成。root_path 参数更适用于把应用部署在路径前缀之后的场景(例如经过反向代理)。

嵌套挂载同样有效

文档还指出:子应用本身也可以再挂载它自己的子应用,一切都能正确工作,因为 FastAPI 会自动处理所有层级的 root_path。也就是说,app → /subapi → /subapi/inner 这样的多级挂载中,每一层的文档 UI 与 OpenAPI 都会获得正确的累积前缀。

延伸阅读

小结

能力 说明
app.mount("/subapi", subapi) 把一个完整独立的 FastAPI 应用挂载到主应用的 /subapi 路径下
独立 OpenAPI 主应用 /openapi.json 与子应用 /subapi/openapi.json 各自独立生成,互不包含对方端点
独立文档 UI /docs/subapi/docs 两个 Swagger UI 可分别交互,各自只列出本应用的路径操作
自动 root_path FastAPI 借助 ASGI root_path 自动为子应用注入前缀:文档 URL、Try it out 的 base URL、OpenAPI servers 字段均自动正确
嵌套挂载 子应用可继续挂载更深层子应用,各层 root_path 自动叠加

适用前提与限制:挂载发生在同一进程内,子应用是标准 ASGI 应用(FastAPI 或任意兼容 ASGIApp 的实例);若你需要的是"路由前缀共享同一个 OpenAPI Schema",应使用 APIRouter(prefix=...);只有需要完全独立的应用边界(独立 Schema、独立文档、独立生命周期与配置)时,才应使用本文介绍的 mount 方式。

登录后查看全文
热门项目推荐
相关项目推荐