FastAPI Sub-Applications Explained: When to Mount a Second App description: Learn how FastAPI sub-applications work, when to use app.mount() instead of APIRouter, how separate docs and OpenAPI schemas behave, and why root_path matters.

0

 

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 /app is served by the main app
  • GET /subapi/sub is served by the mounted app
  • /docs shows the docs for the main app
  • /subapi/docs shows 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 API
  • mount() 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:

  • /docs for the public API
  • /admin/docs for 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_path tells 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-headers
  • root_path handling 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

QuestionAPIRouterMounted sub-application
Is it part of the same FastAPI app?YesNo
Same OpenAPI schema?YesNo
Same docs UI?YesNo
Best for splitting files?YesNot primarily
Best for a separate API surface?NoYes
Works for mounting other ASGI/WSGI apps?NoYes

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/

Post a Comment

0 Comments

Post a Comment (0)

#buttons=(Ok, Go it!) #days=(20)

This site uses cookies from Google to deliver its services and analyze traffic. Your IP address and user-agent are shared with Google along with performance and security metrics to ensure quality of service, generate usage statistics, and to detect and address abuse. More Info
Ok, Go it!