---
title: "FastAPI root_path Does Not Add a Prefix — Starlette Strips It Before Route Matching"
aliases: [fastapi-root-path, starlette-root-path-routes, fastapi-behind-prefix]
tags: [fastapi, starlette, routing, deployment, gotcha, apache, reverse-proxy]
sources:
  - "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---

# FastAPI root_path Does Not Add a Prefix — Starlette Strips It Before Route Matching

When FastAPI is initialised with `root_path="/cc-dashboard"`, Starlette **strips** that prefix from the incoming URL before matching routes. Routes must therefore be registered **without** the prefix. Registering `/cc-dashboard/healthz` results in a permanent 404 — the router never sees the prefix in the path.

## Key Takeaways

- `root_path` is metadata for OpenAPI docs and proxy headers only — it is NOT prepended to route paths
- Starlette removes `root_path` from the URL before route matching; routes see the stripped path
- Register `/healthz`, not `/cc-dashboard/healthz`, even when the app is served at `/cc-dashboard/`
- The Apache `ProxyPass /cc-dashboard/ http://127.0.0.1:8001/` strips the prefix before forwarding — FastAPI's `root_path` merely tells the app "I am mounted at this prefix" for docs generation
- This behaviour is **not documented** on the FastAPI site; it is a Starlette ASGI mount convention

## Details

### The Counterintuitive Initialisation

```python
# app is served by Apache at /cc-dashboard/
app = FastAPI(root_path="/cc-dashboard")

# ✅ CORRECT — Starlette strips root_path, route matches /healthz
@app.get("/healthz")
async def healthz():
    return {"ok": True}

# ❌ WRONG — route registered as /cc-dashboard/healthz
# but Starlette sees /healthz after stripping, so this never matches
@app.get("/cc-dashboard/healthz")
async def healthz_wrong():
    return {"ok": True}
```

### How the Request Flows

```
Browser → GET /cc-dashboard/healthz
    ↓
Apache ProxyPass /cc-dashboard/ http://127.0.0.1:8001/
    ↓ (Apache strips /cc-dashboard prefix)
Starlette receives → GET /healthz   (root_path="/cc-dashboard" stored separately)
    ↓
Route matching: /healthz  ← this is what must be registered
```

### What root_path Actually Does

```python
# root_path is used by Starlette for:
# 1. OpenAPI/Swagger docs — generates correct absolute URLs in the schema
# 2. url_for() — builds URLs with the prefix included
# 3. Request.root_path — accessible in middleware for logging/tracing

app = FastAPI(root_path="/cc-dashboard")
# Swagger UI served at /cc-dashboard/docs will use /cc-dashboard/openapi.json
# url_for("healthz") → "/cc-dashboard/healthz"
```

### Corresponding Apache Config

```apache
# Apache strips /cc-dashboard before proxying
ProxyPass /cc-dashboard/ http://127.0.0.1:8001/
ProxyPassReverse /cc-dashboard/ http://127.0.0.1:8001/

# Trailing slash on BOTH sides is mandatory —
# without it Apache passes the prefix intact → all routes 404
```

> [!warning] ProxyPassMatch stripping variant
> If using `ProxyPassMatch ^/cc-dashboard/(.*)$ http://127.0.0.1:8001/$1`, the `$1` capture group performs the same stripping manually. This is needed when you cannot use the simple `ProxyPass /prefix/ http://host/` form (e.g. when an `Alias` conflict exists — see [[wiki/concepts/apache-mod-alias-proxy-priority]]).

## Related

- [[wiki/concepts/apache-mod-alias-proxy-priority]] — Alias/ProxyPass conflict that forces use of ProxyPassMatch
- [[wiki/concepts/nextjs-basepath-auth-redirects]] — similar prefix-stripping gotcha in Next.js basePath
- [[wiki/tech-patterns/fastapi-python-docker]] — standard Oliver FastAPI deployment pattern

## Sources

- [[daily/2026-05-06.md]] — BAIC dashboard deployment: routes registering with full prefix caused 404s
