The architecture of Starlette, x-rayed
Starlette is the load-bearing wall under FastAPI and much of the modern Python async web, and its x-ray shows a toolkit rather than a framework: 34 loosely-coupled components, exactly 2 runtime dependencies, and a middleware/ package that is the biggest single module because composable middleware is the architecture.
archsteer xray v0.4.1 against encode/starlette@5174d4c (starlette/) on 2026-07-05. Read-only static analysis — no code executed, no architecture rules declared. Structure, not judgment.Module dependency graph
Top 14 modules by connectivity; an arrow means "imports from".
graph LR
m0["_exception_handler"]
m1["_utils"]
m2["applications"]
m3["authentication"]
m4["concurrency"]
m5["datastructures"]
m6["endpoints"]
m7["exceptions"]
m8["middleware"]
m9["requests"]
m10["responses"]
m11["routing"]
m12["types"]
m13["websockets"]
m0 --> m1
m0 --> m4
m0 --> m7
m0 --> m9
m0 --> m12
m0 --> m13
m1 --> m12
m2 --> m5
m2 --> m8
m2 --> m9
m2 --> m10
m2 --> m11
m2 --> m12
m3 --> m1
m3 --> m7
m3 --> m9
m3 --> m10
m3 --> m13
m4 --> m7
m5 --> m4
m5 --> m12
m6 --> m1
m6 --> m4
m6 --> m7
m6 --> m9
m6 --> m10
m6 --> m12
m6 --> m13
m8 --> m0
m8 --> m1
m8 --> m3
m8 --> m4
m8 --> m5
m8 --> m7
m8 --> m9
m8 --> m10
m8 --> m12
m8 --> m13
m9 --> m1
m9 --> m2
m9 --> m5
m9 --> m7
m9 --> m8
m9 --> m11
m9 --> m12
m10 --> m1
m10 --> m4
m10 --> m5
m10 --> m9
m10 --> m12
m11 --> m0
m11 --> m1
m11 --> m4
m11 --> m5
m11 --> m7
m11 --> m8
m11 --> m9
m11 --> m10
m11 --> m12
m11 --> m13
m12 --> m9
m12 --> m10
m12 --> m13
m13 --> m9
m13 --> m10
m13 --> m12Components by module
Most connected components
Top 20 of 34 by exported API surface and dependency count.
| Component | Module | Key exports |
|---|---|---|
routing.py | routing | NoMatchFound, Match, request_response, websocket_session |
testclient.py | testclient | _is_asgi3, _WrapASGI2, _AsyncBackend, _Upgrade |
responses.py | responses | Response, HTMLResponse, PlainTextResponse, JSONResponse |
requests.py | requests | cookie_parser, ClientDisconnect, HTTPConnection, empty_receive |
datastructures.py | datastructures | Address, URL, URLPath, Secret |
_utils.py | _utils | is_async_callable, is_async_callable, is_async_callable, AwaitableOrContextManager |
formparsers.py | formparsers | FormMessage, MultipartPart, _user_safe_decode, MultiPartException |
authentication.py | authentication | has_required_scope, requires, AuthenticationError, AuthenticationBackend |
staticfiles.py | staticfiles | NotModifiedResponse, StaticFiles |
middleware/wsgi.py | middleware | build_environ, WSGIMiddleware, WSGIResponder |
endpoints.py | endpoints | HTTPEndpoint, WebSocketEndpoint |
templating.py | templating | _TemplateResponse, Jinja2Templates |
schemas.py | schemas | OpenAPIResponse, EndpointInfo, BaseSchemaGenerator, SchemaGenerator |
websockets.py | websockets | WebSocketState, WebSocketDisconnect, WebSocket, WebSocketClose |
middleware/sessions.py | middleware | SessionMiddleware, Session |
applications.py | applications | Starlette |
concurrency.py | concurrency | run_until_first_complete, run_in_threadpool, _StopIteration, _next |
middleware/errors.py | middleware | ServerErrorMiddleware |
middleware/base.py | middleware | _CachedRequest, BaseHTTPMiddleware, _StreamingResponse |
convertors.py | convertors | Convertor, StringConvertor, PathConvertor, IntegerConvertor |
Declared runtime dependencies
What the structure teaches
Everything is an ASGI app
Applications, routers, middleware, even individual responses all speak the same (scope, receive, send) interface. That's why the module graph is so flat: components compose by wrapping each other at runtime, not by importing each other's internals.
middleware/ is the largest package for a reason
CORS, sessions, GZip, trusted hosts, HTTPS redirects — each is an isolated component wrapping the next ASGI app in the chain. Starlette's answer to framework features is a stack you assemble, and the x-ray shows each layer as an independent unit.
Two dependencies, total
anyio (async abstraction over asyncio/trio) and typing_extensions. Response objects, routing, websockets, background tasks, static files and the test client are all implemented in-tree — for a toolkit this broad, that's a remarkably small trust surface.
X-ray your own repo
This page is unedited archsteer xray output plus commentary. The same map of your codebase takes one command, runs locally, and never sends code anywhere: pip install archsteer && archsteer xray
FAQ
How is Starlette structured internally?
As a flat toolkit of 34 components where everything implements the ASGI interface: routing.py (the most connected hub, mapping paths to endpoints), applications.py (a thin composition root), a middleware/ package of independent wrappers, plus requests, responses, websockets, and a built-in test client. Only two runtime dependencies: anyio and typing_extensions.
How do Starlette and FastAPI relate?
FastAPI subclasses and delegates to Starlette for everything network-shaped — request parsing, routing mechanics, middleware, websockets, background tasks — and adds its own layers for dependency injection, validation via Pydantic, and OpenAPI generation. Compare the two x-rays: FastAPI's hubs (routing, applications) sit directly on Starlette's.
How was this architecture map generated?
By running `archsteer xray` (an open-source, MIT-licensed CLI) against the starlette/ package directory of the public GitHub repo — read-only static analysis, no code executed. Reproduce it with `pip install archsteer && archsteer xray`.
More x-rays
The architecture of FastAPI, x-rayed
An auto-generated architecture map of FastAPI: 48 components across 28 modules, the module…
The architecture of Flask, x-rayed
An auto-generated architecture map of Flask: 24 components, the sansio core split, bluepri…
The architecture of HTTPX, x-rayed
An auto-generated architecture map of HTTPX: 23 underscore-private components, the pluggab…
The architecture of Pydantic, x-rayed
An auto-generated architecture map of Pydantic: 104 components, the _internal core, the bu…