FastAPI 子应用挂载(Mounts):让多个独立 FastAPI 应用共享一个服务端口
在 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 与 OpenAPIservers字段正确工作。
什么是"挂载"(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.json中paths只包含/app,且没有servers字段;test_openapi_schema_sub断言/subapi/openapi.json中paths只包含/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 都会获得正确的累积前缀。
延伸阅读
root_path的显式用法(例如应用部署在反向代理路径前缀之后)见官方文档 Hinter einem Proxy(Behind a Proxy);- 教程源码:docs_src/sub_applications/tutorial001_py310.py;
- 行为验证测试:tests/test_tutorial/test_sub_applications/test_tutorial001.py;
- 应用类源码:fastapi/applications.py。
小结
| 能力 | 说明 |
|---|---|
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 方式。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

