FastAPI Showcase

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

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 | /<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:

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 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:

---

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*

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: