# FastAPI Showcase > What the FastAPI backend unlocks in this boilerplate — OpenAPI docs, a native JSON API, ASGI middleware, and a path to MCP. --- .. 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 | `//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``, ``//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 ``//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 `//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 tracker.track_visit( request.url.path, request.headers.get("user-agent", ""), ip, ) 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*