# FastAPI Showcase

> What the FastAPI backend unlocks in this boilerplate — OpenAPI docs, a native JSON API, ASGI middleware, and a path to MCP.

**Site index:** [https://boilerplate.2plot.dev/llms.txt](https://boilerplate.2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 8 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://boilerplate.2plot.dev/sitemap.xml  


---

.. llms_copy::FastAPI Showcase

.. toc::

### Why a dedicated showcase?

Most Dash docs assume Flask. With Dash 4.1+ you can swap in a real FastAPI app under the hood, and once you do, things you used to bolt on with extra packages — OpenAPI docs, native async, websocket callbacks, structured JSON APIs — become first-class.

This boilerplate ships both backends side by side. Switch with one env var:

```env
# .env
DASH_BACKEND=fastapi
```

When the badge in the header reads **FastAPI · async**, the surface described below is live.

---

### What lights up on FastAPI

| Surface | Path | Notes |
|--|--|--|
| Swagger UI | `/docs` | Interactive OpenAPI explorer (FastAPI built-in) |
| ReDoc | `/redoc` | Alternative OpenAPI viewer |
| OpenAPI schema | `/openapi.json` | Machine-readable spec |
| Liveness probe | `/healthz` | Returns active backend + Dash version |
| Active backend | `/api/backend` | Backend name, label, async flag |
| Page registry | `/api/pages` | All Dash pages, sortable list with metadata |
| LLM markdown | `/<page>/llms.txt` | Mounted by `dash-improve-my-llms` 2.0 — backend-detected, identical body across Flask/FastAPI/Quart |
| Bot policy | `/robots.txt` | Same surface as the Flask build; same `RobotsConfig` |
| Sitemap | `/sitemap.xml` | Same priority inference; respects `mark_hidden()` |

Everything in the **Surface** column appears in `/docs` because the routes are declared with typed Pydantic models and `response_model=...` — Swagger UI picks them up automatically.

#### Try it live

This widget hits the JSON endpoints directly from your browser and pretty-prints the response. Useful for confirming the surface is up before you point a real client at it.

.. exec::docs.fastapi-showcase.endpoint_explorer
    :code: false

---

### The OpenAPI link in the header

The header surfaces a Swagger badge **only when the active backend is FastAPI**. The logic is small:

```python
# components/header.py
def _create_openapi_link():
    info = get_backend_info()
    if info.name != "fastapi":
        return None
    return dmc.Anchor(
        dmc.Badge("OpenAPI", leftSection=DashIconify(icon="logos:swagger"), ...),
        href="/docs",
        target="_blank",
    )
```

Open it in a new tab and you can `Try it out` against any of the routes without leaving the app.

---

### How the routes are mounted

`run.py` calls `add_llms_routes(app)` unconditionally — dash-improve-my-llms 2.0 detects the FastAPI backend and mounts its own router. The boilerplate's `lib/asgi_routes.py` only carries the **showcase** surfaces (`/healthz`, `/api/backend`, `/api/pages`) — the things that demonstrate first-class OpenAPI integration:


```python
# File: lib/asgi_routes.py

"""
FastAPI showcase routes for the documentation boilerplate.

The AI/LLM surfaces (``/llms.txt``, ``/<page>/llms.txt``, ``/robots.txt``,
``/sitemap.xml``) are mounted by ``dash-improve-my-llms`` 2.0 directly —
the package detects the FastAPI backend and registers its own router.
This module only carries the **showcase** surfaces that demonstrate
first-class OpenAPI integration under Dash 4.1+'s FastAPI backend:

- ``/healthz``       — liveness probe
- ``/api/backend``   — active backend info
- ``/api/pages``     — registered Dash pages, sortable list

These show up in Swagger UI at ``/docs`` and ReDoc at ``/redoc`` because
each route declares a Pydantic ``response_model``.
"""
from __future__ import annotations

from typing import List, Optional

import dash
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel, Field


# ---------------------------------------------------------------------------
# Pydantic models — these power the OpenAPI schema at /docs
# ---------------------------------------------------------------------------


class BackendInfoModel(BaseModel):
    name: str = Field(..., description="Active backend identifier")
    label: str = Field(..., description="Human-readable backend label")
    is_async: bool = Field(..., description="True for ASGI backends (fastapi, quart)")
    description: str


class PageSummary(BaseModel):
    name: str
    path: str
    title: Optional[str] = None
    description: Optional[str] = None
    icon: Optional[str] = None


class PageListResponse(BaseModel):
    backend: str
    count: int
    pages: List[PageSummary]


class HealthResponse(BaseModel):
    ok: bool = True
    backend: str
    dash_version: str


# ---------------------------------------------------------------------------
# Router factories
# ---------------------------------------------------------------------------


def build_api_router(app, backend_info) -> APIRouter:
    """Native FastAPI showcase routes — populate /docs and /redoc."""
    router = APIRouter(prefix="/api", tags=["showcase"])

    @router.get("/backend", response_model=BackendInfoModel, summary="Active backend")
    def get_backend() -> BackendInfoModel:
        return BackendInfoModel(
            name=backend_info.name,
            label=backend_info.label,
            is_async=backend_info.is_async,
            description=backend_info.description,
        )

    @router.get(
        "/pages",
        response_model=PageListResponse,
        summary="Registered Dash pages",
    )
    def list_pages() -> PageListResponse:
        pages: List[PageSummary] = []
        for p in dash.page_registry.values():
            pages.append(PageSummary(
                name=p.get("name"),
                path=p.get("path"),
                title=p.get("title"),
                description=p.get("description"),
                icon=p.get("icon"),
            ))
        return PageListResponse(
            backend=backend_info.name,
            count=len(pages),
            pages=sorted(pages, key=lambda x: x.path),
        )

    return router


def build_health_router() -> APIRouter:
    router = APIRouter(tags=["health"])

    @router.get("/healthz", response_model=HealthResponse, summary="Liveness probe")
    def healthz() -> HealthResponse:
        return HealthResponse(
            ok=True,
            backend="fastapi",
            dash_version=dash.__version__,
        )

    return router


def register_asgi_routes(app, backend_info) -> None:
    """Mount the showcase FastAPI routers on ``app.server``.

    These must be registered **before** ``add_llms_routes(app)`` so that
    the package's catch-all ``/<page>/llms.txt`` matcher does not shadow
    ``/healthz`` or ``/api/*``.
    """
    server: FastAPI = app.server  # type: ignore[assignment]
    server.include_router(build_health_router())
    server.include_router(build_api_router(app, backend_info))
```


Two things to notice:

1. **Pydantic models drive the schema.** `BackendInfoModel`, `PageSummary`, `PageListResponse`, `HealthResponse` — each one shows up in `/docs` with example values.
2. **Route ordering matters.** Mount your showcase routes *before* `add_llms_routes(app)`. The package's `/<page>/llms.txt` is greedy and would otherwise shadow `/api/*` matches.

`run.py` calls `register_asgi_routes(app, BACKEND_INFO)` in the `elif BACKEND == "fastapi":` branch and that's the whole wire-up.

---

### Analytics as ASGI middleware

The Flask build uses `@server.before_request` for the visitor tracker. Under FastAPI, that hook doesn't exist — instead we mount a Starlette middleware:


```python
# File: lib/asgi_middleware.py

"""
ASGI/Starlette middleware ports of Flask-only hooks used in this boilerplate.

When the Dash backend is FastAPI, these slot in where the Flask
``before_request`` decorator was used.
"""
from __future__ import annotations

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response

from lib.analytics_tracker import tracker


class AnalyticsMiddleware(BaseHTTPMiddleware):
    """Track every request through the analytics tracker.

    Mirrors the Flask ``before_request`` shim in ``run.py``. Failures are
    silently swallowed — analytics should never block a real response.
    """

    async def dispatch(self, request: Request, call_next) -> Response:
        try:
            client = request.client
            ip = client.host if client else None
            # Headers carry the real client IP/country behind a proxy or CDN;
            # request.client is the last hop (the proxy) in production.
            tracker.track_visit(
                request.url.path,
                request.headers.get("user-agent", ""),
                ip,
                headers=dict(request.headers),
            )
        except Exception:
            pass
        return await call_next(request)


def register_asgi_middleware(app) -> None:
    """Attach all ASGI middleware to ``app.server`` (a FastAPI instance)."""
    app.server.add_middleware(AnalyticsMiddleware)
```


This sits transparently in front of every request, including Dash's internal `_dash-update-component` calls, with no impact on callback latency (analytics writes go through the same backgroundable tracker).

---

### Async callbacks (you can write them now)

Once you're on FastAPI, callbacks can be `async def` and `await` directly. A typical pattern:

```python
import httpx
from dash import Input, Output, callback

@callback(Output("price", "children"), Input("ticker", "value"))
async def fetch_price(ticker):
    async with httpx.AsyncClient(timeout=5) as client:
        r = await client.get(f"https://api.example.com/{ticker}")
    return r.json()["price"]
```

The key discipline: **don't mix sync I/O into async callbacks**. Replace `requests` with `httpx.AsyncClient`, `psycopg2` with `asyncpg`, `time.sleep` with `asyncio.sleep`. A single blocking call inside an `async def` will stall every other request handled by that worker.

If you can't avoid a blocking call, wrap it:

```python
import asyncio

@callback(Output("data", "children"), Input("trigger", "n_clicks"))
async def slow_path(_):
    return await asyncio.to_thread(legacy_sync_fetch)
```

#### See the difference

This widget runs two callbacks side by side: a sync one that sleeps 3 × 500 ms sequentially (~1500 ms wall-clock) and an async one that fans out the same work with `asyncio.gather` (~500 ms). Both work on any backend, but the async path is what scales when concurrent users hit your app.

.. exec::docs.fastapi-showcase.async_demo
    :code: false

#### Stress test it

The button below fires N parallel `GET /healthz` requests from your browser and reports wall-clock + percentile latencies. On the FastAPI backend the wall-clock stays flat as N rises (the event loop services them concurrently). On Flask's default sync workers, wall-clock grows roughly linearly with N — that's the practical cost of WSGI's one-request-per-thread model.

.. exec::docs.fastapi-showcase.stress_test
    :code: false

---

### Websocket callbacks (Dash 4.2+)

ASGI gives Dash native websocket support. From a callback you can grab the websocket directly via `ctx.websocket` and push updates to the browser without polling.

```python
from dash import Input, Output, callback, ctx

@callback(Output("stream", "children"), Input("start", "n_clicks"))
async def stream(_):
    ws = ctx.websocket
    for i in range(10):
        await ws.send({"i": i})
    return "done"
```

This is gated behind the FastAPI backend — the Flask backend will simply not expose `ctx.websocket`.

---

### MCP server (Dash 4.3+)

When Dash 4.3 ships, the same FastAPI app can expose the dashboard's layout, components, pages, and (whitelisted) callbacks as MCP tools over Streamable HTTP. The wire-up in this boilerplate is best-effort:

```python
# run.py — runs only if the installed Dash supports it
try:
    from dash import mcp_enabled
    HAS_MCP = True
except ImportError:
    HAS_MCP = False

if HAS_MCP and BACKEND == "fastapi" and os.environ.get("DASH_MCP_ENABLED") == "1":
    mcp_enabled(app)
```

Set `DASH_MCP_ENABLED=1` in `.env` once you upgrade to a Dash version that includes MCP, and the server will be reachable at `/mcp` (subject to whatever path Dash settles on for the GA release).

---

### What's missing on FastAPI today

Honesty matters for a docs site:

- **`set_props`** works on FastAPI but if you scale to multiple workers you'll need Redis pub/sub for cross-worker fanout (same caveat as Flask).
- The `dash-improve-my-llms` MCP bridge silently no-ops on Dash 3.x and on Dash 4.1/4.2 — it requires Dash 4.3+ (where `dash.mcp` lands). The HTTP surfaces (`/llms.txt`, `/robots.txt`, `/sitemap.xml`) work on every version.

---

### Deployment

Swap the process manager:

```bash
# Flask (Dockerfile default today)
gunicorn run:server -b 0.0.0.0:8550

# FastAPI
pip install "dash[fastapi]" "uvicorn[standard]"
uvicorn run:server --host 0.0.0.0 --port 8550 \
    --workers 4 --proxy-headers --forwarded-allow-ips='*'
```

The `app.server` attribute returns either a `Flask` or `FastAPI` instance depending on `DASH_BACKEND`, so `run:server` works for both — only the runner changes.

---

### Recap

| Concern | Before | After |
|--|--|--|
| Backend selection | hardcoded Flask | `DASH_BACKEND` env var |
| OpenAPI surface | none | `/docs`, `/redoc`, `/openapi.json` |
| Health check | none | `/healthz` |
| JSON API for tooling | none | `/api/backend`, `/api/pages` |
| Async callbacks | thread-pooled | native `async def` |
| Websocket callbacks | not available | `ctx.websocket` |
| MCP integration | not available | best-effort, gated on `DASH_MCP_ENABLED` |
| LLM routes | `add_llms_routes()` | ported in `lib/asgi_routes.py` (subset) |
| Analytics | `before_request` | Starlette middleware |

Flip the env var, restart, watch the badge change. That's the whole user surface — everything else is a clean import switch under the hood.


---

*Source: /fastapi-showcase*
