FastAPI makes it very easy to split a project into modules with APIRouter. But sometimes a router is not enough. Sometimes you need a second application inside the first one.
That is exactly what FastAPI sub-applications are for.
A sub-application is a fully independent ASGI app mounted under a path prefix, such as /admin, /internal, or /v2. FastAPI calls this mounting.
app.mount("/subapi", subapi)
At first glance, this can look similar to include_router(). In practice, it solves a different problem entirely.
This article explains what sub-applications are, how app.mount() works, when it is a better choice than APIRouter, and what to watch out for when deploying behind a proxy.
What is a FastAPI sub-application?
A sub-application is another FastAPI app that lives under a path prefix of a parent app.
That means you can have one main application:
//health/docs
And another fully separate FastAPI app mounted under:
/subapi/subapi/docs/subapi/openapi.json
The important point is this: the mounted app is not just a group of routes. It is treated as a separate application.
That is why mounted apps are useful when you want:
- separate API documentation
- a separate OpenAPI schema
- a different lifecycle boundary in your architecture
- a clean split between public and internal surfaces
- to mount another ASGI or WSGI application under a path
Minimal example
Here is the simplest working example.
from fastapi import FastAPI
app = FastAPI(title="Main API")
@app.get("/app")
def read_main():
return {"message": "Hello from main app"}
subapi = FastAPI(title="Sub API")
@subapi.get("/sub")
def read_sub():
return {"message": "Hello from sub app"}
app.mount("/subapi", subapi)
With this setup:
GET /appis served by the main appGET /subapi/subis served by the mounted app/docsshows the docs for the main app/subapi/docsshows the docs for the mounted app
That split is the biggest conceptual difference from APIRouter.
Why not just use APIRouter?
In most projects, APIRouter is the right tool.
Use APIRouter when you want to split one application into multiple files while still keeping everything inside the same API surface and the same OpenAPI schema.
Example:
from fastapi import APIRouter, FastAPI
users_router = APIRouter(prefix="/users", tags=["users"])
@users_router.get("/")
def list_users():
return [{"id": 1, "name": "Ada"}]
app = FastAPI()
app.include_router(users_router)
Here, /users/ becomes part of the same app. The routes appear in the same /docs and the same /openapi.json.
Now compare that with mount():
app.mount("/users", users_app)
This does not merge route operations into the same schema. Instead, everything under /users is handed off to another application.
A practical rule
Use:
include_router()for modularity inside one APImount()for a separate application under a prefix
That distinction stays useful even in large systems.
What actually happens when you mount a sub-app?
When FastAPI mounts a sub-application, the parent app delegates the whole path branch to the child app.
If the mount point is /subapi, then requests under /subapi/... are handled by that mounted app.
This is why the docs stay separate.
The main app knows nothing about the sub-app’s route operations as part of its own OpenAPI schema. The child app exposes its own documentation UI and its own openapi.json.
That makes mounted apps a strong fit for boundaries such as:
- public API vs internal admin API
- customer API vs operator API
- current API vs experimental API
- FastAPI plus a mounted legacy Flask or Django app
Separate docs and OpenAPI are the big feature
This is the feature most teams care about first.
With routers, all endpoints show up together in one Swagger UI.
With sub-applications:
- the parent app keeps its own docs
- the child app gets its own docs under the mounted prefix
- both apps can have their own title, description, version, and schema
Example:
from fastapi import FastAPI
app = FastAPI(
title="Public API",
version="1.0.0",
)
admin_app = FastAPI(
title="Admin API",
version="1.0.0",
)
@app.get("/status")
def public_status():
return {"status": "ok"}
@admin_app.get("/metrics")
def admin_metrics():
return {"requests": 128}
app.mount("/admin", admin_app)
Now you effectively have:
/docsfor the public API/admin/docsfor the admin API
That makes the developer experience much cleaner when the audiences are different.
A realistic structure for a larger codebase
When a project grows, keeping mounted apps in separate modules becomes clearer than building everything in one file.
# main.py
from fastapi import FastAPI
from public_app import public_app
from admin_app import admin_app
app = FastAPI(title="Gateway API")
app.mount("/api", public_app)
app.mount("/admin", admin_app)
# public_app.py
from fastapi import FastAPI
public_app = FastAPI(title="Public API")
@public_app.get("/health")
def health():
return {"status": "ok"}
# admin_app.py
from fastapi import FastAPI
admin_app = FastAPI(title="Admin API")
@admin_app.get("/jobs")
def jobs():
return {"running": 3}
This pattern is especially useful when each app has a different audience, ownership model, or release cadence.
root_path is the detail that makes mounted docs work
FastAPI automatically handles the mounted prefix by using the ASGI concept called root_path.
That matters because the child app needs to know that it lives under /subapi or /admin rather than at the domain root.
Without that, the docs UI inside the sub-app would generate incorrect URLs.
FastAPI takes care of this automatically for mounted apps, which is why /subapi/docs works correctly without extra configuration in the basic case.
So when the child app builds links to its schema or serves its docs interface, it understands that it is not living at /, but under the mounted prefix.
Why root_path becomes more important behind proxies
Things get more interesting once you deploy behind Nginx, Traefik, or another reverse proxy.
If the proxy rewrites paths, strips a prefix, or terminates HTTPS in front of your app, your application may need correct forwarded headers and a correct root_path setup.
A common example is serving the app externally at:
https://example.com/api/v1
while the FastAPI server itself listens internally at:
http://127.0.0.1:8000
In setups like that:
- forwarded headers tell the app about the original host and scheme
root_pathtells the app about the path prefix
This is especially important for redirects and docs URLs.
If you are behind a trusted proxy, review:
- forwarded headers configuration
--forwarded-allow-ips--proxy-headersroot_pathhandling when a prefix is stripped or injected
Sub-applications already use root_path internally for mounted prefixes, but proxy configuration can still affect the final URLs that clients see.
Common use cases
Here are the cases where mount() usually makes sense.
1. Public API and internal API in the same process
You may want public endpoints for customers and separate operational endpoints for admins or support staff.
Mounted apps let you keep:
- separate docs
- separate schema branding
- separate route trees
without running a second process.
2. Versioned APIs with stronger isolation
If you are running a legacy API and a new API side by side, you may prefer:
/v1/v2
as separate mounted apps rather than one merged schema.
This can reduce confusion in the docs and make migration cleaner.
3. Mounting non-FastAPI apps
Because mounting is part of the ASGI routing model, the mounted target does not have to be another FastAPI app.
You can mount:
StaticFiles- another Starlette app
- a WSGI app wrapped with
WSGIMiddleware
That makes mounting useful during migrations from Flask or Django to FastAPI.
Common mistakes
Treating mount() as a prettier include_router()
That is the most common misunderstanding.
If you only want to split files or organize routes, use APIRouter.
If you use mount() just for code organization, you may end up with separate docs and schema boundaries you did not actually want.
Forgetting about docs URLs
Teams sometimes mount an app under /admin and then wonder why its docs are no longer at /docs.
They are now under the mounted prefix:
/admin/docs
Ignoring proxy behavior
A mounted app may work perfectly in local development but break in production if:
- the proxy strips a prefix
- forwarded headers are not trusted
- redirects generate the wrong host or scheme
This is usually not a sub-application bug. It is a deployment configuration issue.
APIRouter vs sub-application: quick comparison
| Question | APIRouter | Mounted sub-application |
|---|---|---|
| Is it part of the same FastAPI app? | Yes | No |
| Same OpenAPI schema? | Yes | No |
| Same docs UI? | Yes | No |
| Best for splitting files? | Yes | Not primarily |
| Best for a separate API surface? | No | Yes |
| Works for mounting other ASGI/WSGI apps? | No | Yes |
A good mental model
Think of APIRouter as composition inside one app.
Think of mount() as delegation to another app.
That mental model helps you make the right choice quickly.
If you want one API with one schema, use routers.
If you want a second application under a path prefix, mount it.
Final thoughts
FastAPI sub-applications are not something you need every day, but when you do need them, they solve a very specific architectural problem cleanly.
They are ideal when you want:
- independent documentation
- independent OpenAPI schemas
- stronger separation between route groups
- a migration path for legacy apps
- a path-based multi-app setup in one server process
For regular project structure, keep using APIRouter.
For a truly separate app under a prefix, use app.mount().
That is the difference.
References
- FastAPI: Sub Applications - Mounts - fastapi.tiangolo.com/advanced/sub-applications/
- FastAPI: Bigger Applications - Multiple Files - fastapi.tiangolo.com/tutorial/bigger-applications/
- FastAPI: Behind a Proxy - fastapi.tiangolo.com/advanced/behind-a-proxy/
- FastAPI: Including WSGI - Flask, Django, others - fastapi.tiangolo.com/advanced/wsgi/
- Starlette Routing - www.starlette.io/routing/