FastAPI CLI, Reverse Proxies, GraphQL & AI Development
FastAPI v0.162.2 unifies local-to-production server operations (fastapi dev / fastapi run and ASGI --root-path proxy routing), hybrid Strawberry GraphQL sub-routers sharing Depends(), and bundled AI coding agent skills (uvx library-skills + PEP 727 Doc() + SSE/JSONL streaming).
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.smartarts import CardList, Panel
from drawlib.styles import Styles
from drawlib.text import text
setup(width=198, height=90, dpi=150)
text(
(99.0, 85.0),
"FastAPI CLI Modes & Reverse Proxy (--root-path) Operational Topology",
style=Styles.DarkBold.patch(text_size=13.0),
)
# Left Panel: FastAPI CLI
cli_panel = Panel(
"1. FastAPI CLI (fastapi[standard])",
style=Styles.Neutral.patch(shape_r=2.0),
header_style=Styles.SecondaryNeutral.patch(shape_line_width=1.2),
text_style=Styles.DarkBold.patch(text_size=11.0),
icon=phosphor.terminal_window,
icon_style=Styles.SecondaryBold,
header_variant="filled",
header_height=8.5,
)
cli_panel.draw((4.0, 5.0), width=82.0, height=74.0)
l_xy, l_w, l_h = cli_panel.get_content_box((4.0, 5.0), width=82.0, height=74.0, padding=(3.5, 3.0))
cl_cli = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=10.5),
description_style=Styles.Dark.patch(text_size=10.0),
badge_shape="rounded_rect",
badge_style=Styles.SecondaryNeutral,
badge_text_style=Styles.DarkBold.patch(text_size=10.0),
icon_style=Styles.SecondaryBold,
direction="vertical",
spacing=2.8,
)
cl_cli.add(
"[tool.fastapi] Entrypoint",
"pyproject.toml -> 'app.main:app'",
badge_text="TOML",
icon=phosphor.file_code,
)
cl_cli.add(
"fastapi dev",
"127.0.0.1 • Reload • FASTAPI_ENV=dev",
badge_text="DEV",
icon=phosphor.code,
style=Styles.SecondaryNeutral,
)
cl_cli.add(
"fastapi run",
"Prod Uvicorn • --workers • --proxy-headers",
badge_text="RUN",
icon=phosphor.rocket_launch,
style=Styles.PrimaryNeutral,
badge_style=Styles.PrimaryFlat,
badge_text_style=Styles.WhiteBold.patch(text_size=10.0),
icon_style=Styles.WhiteBold,
)
cl_cli.draw(xy=l_xy, width=l_w, height=l_h)
# Right Panel: Behind a Reverse Proxy & --root-path
proxy_panel = Panel(
"2. TLS Reverse Proxy & --root-path /api/v1",
style=Styles.Neutral.patch(shape_r=2.0),
header_style=Styles.PrimaryNeutral.patch(shape_line_width=1.2),
text_style=Styles.DarkBold.patch(text_size=11.0),
icon=phosphor.globe,
icon_style=Styles.PrimaryBold,
header_variant="filled",
header_height=8.5,
)
proxy_panel.draw((90.0, 5.0), width=104.0, height=74.0)
# Top ingress boxes inside Right Panel
rectangle((115.0, 58.5), width=42.0, height=15.5, style=Styles.White.patch(shape_r=1.8))
text((115.0, 61.5), "TLS Edge Proxy", style=Styles.DarkBold.patch(text_size=10.5))
text((115.0, 55.8), "Strips /api/v1", style=Styles.Dark.patch(text_size=10.0))
line((136.5, 58.5), (144.5, 58.5), arrow_head="->", style=Styles.PrimaryBold.patch(line_width=1.8))
rectangle((167.5, 58.5), width=45.0, height=15.5, style=Styles.PrimaryFlat.patch(shape_r=1.8))
text((167.5, 61.5), "--root-path /api/v1", style=Styles.WhiteBold.patch(text_size=10.5))
text((167.5, 55.8), "scope['root_path']", style=Styles.White.patch(text_size=10.0))
line((167.5, 50.5), (167.5, 44.5), arrow_head="->", style=Styles.DarkBold.patch(line_width=1.6))
# Middle OpenAPI resolution card
rectangle((142.0, 35.5), width=96.0, height=16.5, style=Styles.PrimaryNeutral.patch(shape_r=1.8))
phosphor.file_text((100.0, 35.5), width=5.0, style=Styles.PrimaryBold)
text(
(105.5, 38.5),
"Per-Request OpenAPI & Swagger UI Resolution",
style=Styles.DarkBold.patch(text_size=10.5, halign="left"),
)
text(
(105.5, 32.5),
"servers: [{'url': '/api/v1'}] • /docs -> /api/v1/openapi.json",
style=Styles.Dark.patch(text_size=10.0, halign="left"),
)
# Bottom Edge Middleware & v0.146.0 Deprecation card
rectangle((142.0, 15.5), width=96.0, height=16.5, style=Styles.SecondaryNeutral.patch(shape_r=1.8))
phosphor.shield_check((100.0, 15.5), width=5.0, style=Styles.SecondaryBold)
text(
(105.5, 18.5),
"Edge Middleware & v0.146.0+ Server Rule",
style=Styles.DarkBold.patch(text_size=10.5, halign="left"),
)
text(
(105.5, 12.5),
"TrustedHost • HTTPSRedirect • GZip • Avoid app root_path",
style=Styles.Dark.patch(text_size=10.0, halign="left"),
)
save()
1. FastAPI CLI, Reverse Proxies (--root-path) & Edge Middleware
Installing pip install "fastapi[standard]" equips the fastapi command (fastapi.cli:main delegating to fastapi-cli), replacing ad-hoc shell scripts with standardized local development, production ASGI execution, and optional managed cloud deployment (uv run fastapi deploy).
Declaring [tool.fastapi] & Comparing fastapi dev vs fastapi run
Rather than repeating module paths on every command line, register your application object once in pyproject.toml:
[tool.fastapi]
entrypoint = "my_app.main:app"
With entrypoint configured, fastapi dev and fastapi run apply distinct security and concurrency defaults tailored to each environment:
| Capability | fastapi dev (Local Development) |
fastapi run (Production Runtime) |
|---|---|---|
| Default Bind Address | 127.0.0.1:8000 (safe local loopback only) |
0.0.0.0:8000 (container interface; restrict behind proxy) |
| Auto-Reload & Watchers | Enabled (restarts instantaneously on file edits) | Disabled (zero filesystem polling overhead) |
FASTAPI_ENV Variable |
Sets FASTAPI_ENV="development" if unset |
Unset (app.frontend(check_dir="auto") enforces build check) |
| Scaling & Proxy Flags | Single reload process | --workers N, --proxy-headers, --root-path /api/v1 |
Reverse Proxies & the v0.146.0 Deprecation of FastAPI(root_path=...)
In microservice gateways (Nginx, Traefik, Envoy, Cloud Load Balancing), an external proxy frequently routes https://api.example.com/api/v1/catalog to an internal container while stripping the /api/v1 prefix so the container receives /catalog. Without knowing that external prefix, Swagger UI (/docs) would try to fetch /openapi.json at the root domain instead of /api/v1/openapi.json and fail with 404.
Starting in FastAPI v0.146.0 (and enforced with FastAPIDeprecationWarning in v0.162.2), passing FastAPI(root_path="/api/v1") in Python code is deprecated. Mount prefixes belong to deployment topology—not application source code—so configure the ASGI server via CLI instead:
fastapi run --proxy-headers --root-path /api/v1
Inside FastAPI.setup(), v0.162.2 reads req.scope.get("root_path", "") dynamically on every /openapi.json, /docs, and /redoc request:
- Zero Cache Poisoning:
app.openapi()caches the base OpenAPI3.1.0dictionary once, and the/openapi.jsonroute shallow-copies that dictionary per request to prepend{"url": "/api/v1"}toservers(unlessroot_path_in_servers=False)—ensuring a spoofed or tenant-specificroot_pathnever mutatesapp.openapi_schemafor other requests. - Automatic Docs UI Rewriting: Swagger UI (
/docs), OAuth2 redirect (/docs/oauth2-redirect), and ReDoc (/redoc) automatically prefixopenapi_urlwithroot_path(/api/v1/openapi.json).
Built-in Edge Security & Compression Middlewares
Alongside custom pure ASGI middlewares, fastapi.middleware re-exports battle-tested edge middlewares:
TrustedHostMiddleware(allowed_hosts=[...]): Defends against HTTPHostheader poisoning attacks by rejecting unrecognizedHostheaders with400 Bad Request.HTTPSRedirectMiddleware: Enforces307 Temporary Redirectfromhttp://tohttps://(orws://towss://) when TLS is not already enforced at an upstream load balancer.GZipMiddleware(minimum_size=1000, compresslevel=5): Compresses responses exceedingminimum_sizebytes whenever the client sendsAccept-Encoding: gzip.
import warnings
from fastapi import FastAPI, Request
from fastapi.exceptions import FastAPIDeprecationWarning
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.testclient import TestClient
def build_edge_app(*, enforce_https: bool = False) -> FastAPI:
app = FastAPI(title="Edge Catalog API", version="1.0.0")
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["api.example.com", "testserver"],
)
app.add_middleware(GZipMiddleware, minimum_size=500, compresslevel=5)
if enforce_https:
app.add_middleware(HTTPSRedirectMiddleware)
@app.get("/catalog")
def read_catalog(request: Request) -> dict[str, object]:
return {
"root_path": request.scope.get("root_path", ""),
"items": [f"sku-{idx:03d}-enterprise-edition" for idx in range(40)],
}
return app
def test_root_path_openapi_servers_and_edge_middlewares() -> None:
app = build_edge_app()
# Simulate `fastapi run --root-path /api/v1` via ASGI scope root_path
with TestClient(app, root_path="/api/v1") as prefixed_client:
res = prefixed_client.get("/catalog", headers={"Accept-Encoding": "gzip"})
assert res.status_code == 200
assert res.headers.get("content-encoding") == "gzip"
assert res.json()["root_path"] == "/api/v1"
schema = prefixed_client.get("/openapi.json").json()
assert schema["servers"] == [{"url": "/api/v1"}]
assert "/api/v1/openapi.json" in prefixed_client.get("/docs").text
# Unprefixed requests remain clean; spoofed Host headers return 400
with TestClient(app) as direct_client:
assert "servers" not in direct_client.get("/openapi.json").json()
bad_host = direct_client.get("/catalog", headers={"Host": "evil.attacker.tld"})
assert bad_host.status_code == 400
# HTTPSRedirectMiddleware redirects plain HTTP -> HTTPS (307)
https_app = build_edge_app(enforce_https=True)
with TestClient(https_app, base_url="http://api.example.com") as http_client:
redir = http_client.get("/catalog", follow_redirects=False)
assert redir.status_code == 307
assert redir.headers["location"] == "https://api.example.com/catalog"
# Passing root_path to FastAPI(...) emits FastAPIDeprecationWarning
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
FastAPI(root_path="/legacy")
assert any(issubclass(w.category, FastAPIDeprecationWarning) for w in caught)
2. Strawberry GraphQL & AI-Native Development Workflow
FastAPI's type-annotation design extends naturally to both typed GraphQL query graphs via Strawberry and modern AI Coding Agents / MCP tool ecosystems.
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.smartarts import CardList, Panel
from drawlib.styles import Styles
from drawlib.text import text
setup(width=192, height=90, dpi=150)
text(
(96.0, 85.0),
"Strawberry GraphQL DI Integration & FastAPI v0.162.2 AI Agent Workflow",
style=Styles.DarkBold.patch(text_size=13.0),
)
# Left Panel: Strawberry GraphQL
gql_panel = Panel(
"1. Strawberry GraphQLRouter + FastAPI DI",
style=Styles.Neutral.patch(shape_r=2.0),
header_style=Styles.PrimaryNeutral.patch(shape_line_width=1.2),
text_style=Styles.DarkBold.patch(text_size=11.0),
icon=phosphor.graph,
icon_style=Styles.PrimaryBold,
header_variant="filled",
header_height=8.5,
)
gql_panel.draw((4.0, 5.0), width=90.0, height=74.0)
l_xy, l_w, l_h = gql_panel.get_content_box((4.0, 5.0), width=90.0, height=74.0, padding=(3.5, 3.0))
cl_gql = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=10.5),
description_style=Styles.Dark.patch(text_size=10.0),
badge_shape="rounded_rect",
badge_style=Styles.PrimaryNeutral,
badge_text_style=Styles.PrimaryBold.patch(text_size=10.0),
icon_style=Styles.PrimaryBold,
direction="vertical",
spacing=2.8,
)
cl_gql.add(
"Shared FastAPI Depends()",
"OAuth2 Viewer • SQLModel Session • Settings",
badge_text="DI",
icon=phosphor.plugs,
)
cl_gql.add(
"context_getter Bridge",
"BaseContext injects DI into info.context",
badge_text="CTX",
icon=phosphor.arrows_left_right,
style=Styles.PrimaryNeutral,
)
cl_gql.add(
"include_router(..., '/graphql')",
"Typed @strawberry.type alongside REST",
badge_text="GQL",
icon=phosphor.brackets_curly,
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=10.5),
description_style=Styles.White.patch(text_size=10.0),
badge_style=Styles.White,
badge_text_style=Styles.PrimaryBold.patch(text_size=10.0),
icon_style=Styles.PrimaryBold,
)
cl_gql.draw(xy=l_xy, width=l_w, height=l_h)
# Right Panel: Develop with AI Workflow
ai_panel = Panel(
"2. Develop with AI & Bundled Agent Skills",
style=Styles.Neutral.patch(shape_r=2.0),
header_style=Styles.SecondaryNeutral.patch(shape_line_width=1.2),
text_style=Styles.DarkBold.patch(text_size=11.0),
icon=phosphor.sparkle,
icon_style=Styles.SecondaryBold,
header_variant="filled",
header_height=8.5,
)
ai_panel.draw((98.0, 5.0), width=90.0, height=74.0)
r_xy, r_w, r_h = ai_panel.get_content_box((98.0, 5.0), width=90.0, height=74.0, padding=(3.5, 3.0))
cl_ai = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=10.5),
description_style=Styles.Dark.patch(text_size=10.0),
badge_shape="rounded_rect",
badge_style=Styles.SecondaryNeutral,
badge_text_style=Styles.DarkBold.patch(text_size=10.0),
icon_style=Styles.SecondaryBold,
direction="vertical",
spacing=2.8,
)
cl_ai.add(
"uvx library-skills",
"Installs bundled fastapi/.agents/skills",
badge_text="1",
icon=phosphor.robot,
style=Styles.SecondaryNeutral,
)
cl_ai.add(
"Doc() + OpenAPI 3.1 + llms.txt",
"PEP 727 contracts for agents & SDKs",
badge_text="2",
icon=phosphor.book_open,
)
cl_ai.add(
"AI & MCP Streaming",
"POST EventSourceResponse & JSONL yield",
badge_text="3",
icon=phosphor.broadcast,
style=Styles.PrimaryNeutral,
badge_style=Styles.PrimaryFlat,
badge_text_style=Styles.WhiteBold.patch(text_size=10.0),
icon_style=Styles.WhiteBold,
)
cl_ai.draw(xy=r_xy, width=r_w, height=r_h)
save()
Mounting Strawberry (GraphQLRouter) with Shared Depends()
GraphQL excels when frontend views traverse deeply nested object graphs in a single round-trip, while standard REST operations remain simpler for HTTP edge caching, typed OpenAPI SDK generation, file uploads, and status-code semantics. Rather than running a separate GraphQL server, Strawberry (strawberry-graphql) mounts directly onto FastAPI as an APIRouter via GraphQLRouter:
- Type-Hint Symmetry:
@strawberry.typeclasses mirror Pythondataclassesand Pydantic models using native type annotations. - Unified Dependency Injection (
context_getter): Subclassingstrawberry.fastapi.BaseContextand passing an async or synccontext_getterfunction lets GraphQL resolvers consume the exact same FastAPIDepends()dependencies (OAuth2 user extractors, SQLModelSessionDep, andSettingsDep)—meaningapp.dependency_overridesautomatically controls both REST and/graphqlinpytest!
Developing with AI in FastAPI v0.162.2 (library-skills, Doc() & MCP SSE)
Because LLMs pre-trained on older corpora often emit outdated patterns (such as deprecated ORJSONResponse, RootModel, bare Depends() defaults, or legacy @app.on_event("startup")), FastAPI v0.162.2 ships official, version-pinned AI Agent Skills directly inside the fastapi wheel (fastapi/.agents/skills/fastapi/SKILL.md + references/):
- Sync Version-Matched Skills via
uvx library-skills: Runninguvx library-skillsextracts the exact.agents/skills/fastapi/rules bundled with your installedfastapiversion into your workspace so coding agents automatically applyAnnotated[..., Depends()],app.frontend(), native OpenTelemetry,EventSourceResponse,SQLModel, andAsyncer(asyncify/syncify). - Machine-Readable Contracts (
PEP 727 Doc(), OpenAPI3.1.0&llms.txt): Annotating parameters withAnnotated[str, Doc("...")]enriches/openapi.json(3.1.0JSON Schema), allowing agent tool registries, OpenAPI client generators, andllms.txtdocumentation indexes to inspect exact types and constraints without hallucinating parameters. - AI & Model Context Protocol (MCP) Streaming: FastAPI's native Server-Sent Events (
EventSourceResponse) and JSON Lines work seamlessly onPOSTas well asGET, makingresponse_class=EventSourceResponsewithyieldready out-of-the-box for LLM token streaming and MCP HTTP+SSE transports.
from collections.abc import AsyncIterable
from typing import Annotated
from annotated_doc import Doc
from fastapi import Depends, FastAPI
from fastapi.sse import EventSourceResponse, ServerSentEvent
from fastapi.testclient import TestClient
from pydantic import BaseModel
import strawberry
from strawberry.fastapi import BaseContext, GraphQLRouter
def get_current_viewer() -> str:
return "alice@corp.internal"
ViewerDep = Annotated[str, Depends(get_current_viewer)]
class GraphQLContext(BaseContext):
def __init__(self, viewer: str) -> None:
super().__init__()
self.viewer = viewer
async def get_graphql_context(viewer: ViewerDep) -> GraphQLContext:
return GraphQLContext(viewer=viewer)
@strawberry.type
class BookNode:
title: str
author: str
requested_by: str
@strawberry.type
class QueryRoot:
@strawberry.field
def books(self, info: strawberry.Info[GraphQLContext, None]) -> list[BookNode]:
return [
BookNode(
title="FastAPI Architecture",
author="Sebastián Ramírez",
requested_by=info.context.viewer,
)
]
graphql_schema = strawberry.Schema(query=QueryRoot)
graphql_router = GraphQLRouter(graphql_schema, context_getter=get_graphql_context)
app = FastAPI(title="Hybrid REST + GraphQL + AI Gateway")
app.include_router(graphql_router, prefix="/graphql", tags=["graphql"])
class AgentPrompt(BaseModel):
prompt: Annotated[str, Doc("Natural-language instruction sent by the AI agent.")]
@app.post("/ai/stream", response_class=EventSourceResponse)
async def stream_agent_steps(
body: AgentPrompt,
viewer: ViewerDep,
) -> AsyncIterable[ServerSentEvent]:
yield ServerSentEvent(
event="agent.step",
id="1",
data={"viewer": viewer, "accepted_prompt": body.prompt},
)
def test_strawberry_graphql_di_and_ai_sse() -> None:
app.dependency_overrides[get_current_viewer] = lambda: "bot@ci.local"
try:
with TestClient(app) as client:
gql_res = client.post(
"/graphql",
json={"query": "{ books { title requestedBy } }"},
)
assert gql_res.status_code == 200
assert gql_res.json() == {
"data": {
"books": [
{
"title": "FastAPI Architecture",
"requestedBy": "bot@ci.local",
}
]
}
}
sse_res = client.post("/ai/stream", json={"prompt": "Summarize schema"})
assert sse_res.status_code == 200
assert sse_res.headers["content-type"].startswith("text/event-stream")
assert "event: agent.step" in sse_res.text
finally:
app.dependency_overrides.clear()
Review pydantic-settings & SQL Databases for typed environment configuration and SQLModel testing, or return to the Overview & Architecture Map for the complete FastAPI v0.162.2 curriculum.