Bigger applications¶
Real FastAPI applications are split across modules: APIRouters included with prefixes, an app built by a
factory function, sub-applications mounted under a path. py2axum resolves the imports statically and
registers the routes in the order Python would, then routes requests like Starlette.
The documentation's examples are one application split in modules: each page of this tutorial has
its APIRouter in docs_src/tutorial/, and docs_src/main.py includes them all. Imports between them are
relative (from ..db import SessionDep) and resolved statically. Like every example on this site, it is compiled and compared with FastAPI in CI (how).
docs_src/main.py
"""The application of the documentation's examples: each page of the tutorial has its router in tutorial/.
uvicorn docs_src.main:app # the Python application, from the repository's root
py2axum docs_src --root . -o build/docs_axum # the same application, compiled
"""
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from .db import Base, engine
from .tutorial import (background, body, dependencies, errors, files, models, parameters, responses, security, sql,
websockets)
@asynccontextmanager
async def lifespan(app: FastAPI):
# before the server listens: create the tables that do not exist yet
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
# after the server has stopped (SIGTERM, Ctrl+C)
print("shutting down")
app = FastAPI(title="py2axum documentation examples", lifespan=lifespan)
@app.middleware("http")
async def add_served_by(request: Request, call_next):
response = await call_next(request)
response.headers["X-Served-By"] = "docs-example"
return response
class RequestIdMiddleware(BaseHTTPMiddleware):
"""Echo the caller's request id, or say there was none."""
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers["X-Request-Id"] = request.headers.get("x-request-id", "none")
return response
app.add_middleware(RequestIdMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.org"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["authorization", "content-type"],
)
app.add_exception_handler(errors.UnicornException, errors.unicorn_handler)
app.include_router(parameters.router)
app.include_router(body.router)
app.include_router(models.router)
app.include_router(responses.router)
app.include_router(errors.router)
app.include_router(dependencies.router)
app.include_router(security.router)
app.include_router(sql.router)
app.include_router(files.router)
app.include_router(background.router)
app.include_router(websockets.router)
@app.get("/health")
async def health() -> dict:
return {"status": "ok"}
What is native¶
FastAPI()apps,APIRouter(prefix=, tags=, dependencies=),include_router, routers split across modules, apps built by a factory function (create_app()), including endpoints defined inside it; a local of the factory assigned once is evaluated once.ifstatements around registrations are evaluated at startup in the factory's scope (e.g.if not settings.TESTING:); the factory's parameters have their default values (the server calls it without arguments).include_router(router, prefix=<expression>)at module level (e.g.prefix=settings.API_V1_PREFIX): the prefix is evaluated at startup, after the module globals, so the environment still overrides the settings; it is checked like FastAPI (must start with/, must not end with/; a failure stops the binary as an import error stops uvicorn).- Imports are resolved statically, without executing code: relative imports, re-exports from
__init__, lazy imports inside functions (visible from nested functions and lambdas),--rootassys.path. - Routing like Starlette: declaration order, decoded path, first match on path and method wins, then 405
with the methods of the first path match, then a 307 redirect with/without the trailing slash, then 404.
No implicit HEAD. The redirect uses
http://(as uvicorn does without trusted proxy headers). - Raw ASGI routes:
app.add_route(path, obj, methods=...)/app.router.add_route(...)at module level, with an instance of a project class definingasync def __call__(self, scope, receive, send)(Starlette runs anything that is not a function or a method as an ASGI app). The app gets an ASGI 3scopedict (the keys uvicorn and Starlette's router set,scope["app"]with an emptydependency_overrides),receive(the body in onehttp.requestmessage, then nothing until the client leaves) andsend(http.response.start/http.response.body, streamed whenmore_bodyis true).Request(scope, receive)(the request being served only), a response object called as an ASGI app (await JSONResponse(...)(scope, receive, send)), a generator dependency called by hand (gen = get_session(); s = await anext(gen); ...; await gen.aclose()). - Routing objects as values:
request.app,app.router.routesas FastAPI 0.141+ lists them (its docs routes, an_IncludedRouterperinclude_routerwith itsoriginal_router,APIRoutes, the addedRoutes), theirpath,path_format,methods,name,matches(scope)returningstarlette.routing.Matchand the child scope,isinstance(r, Route | APIRoute).request.scopeis a snapshot dict withtype,http_version,scheme,method,root_path,path,raw_path,query_string,headers,client,app, and, once the router has run,path_paramsandroute(theAPIRoute, as Starlette sets it); theendpointof scopes is None.FastAPI(docs_url=...)and the other*_urloptions must be literals forapp.routesto be read.
Routes added by a configure(app) function are described in Middleware.
Mounted applications¶
app.mount(path, X)(a sub-application,StaticFiles, FastMCP'sstreamable_http_app()...) is not translated, but the app's last registrations may be mounts left to the Python side (--python-side mount, implied by--python-side auto): withPY2AXUM_PYTHON_URLset, a request under the mount's path (or the path itself, for Starlette's 307 topath/) that no translated route fully matches (path and method) is relayed to the Python application. As in Starlette, a mount's full match beats an earlier partial one: a HEAD, or another method, on a translated GET route under the mount reaches the mounted application, not a- A non-literal mount path relays every unmatched request. Refused with
file:line: a mount followed by another registration (include_router,@app.get,add_api_route,add_route...: a route after the mount is shadowed by it in Python but would be served by the binary), or registrations of the app in another module or function than the mount (their order depends on imports or calls). WebSocket handshakes under the mount are not relayed (see WebSockets). FastAPI(root_path="/api/v1")(a literal;root_path_in_servers=only changes the OpenAPI schema): the scope'sroot_path, stripped from the path before routing when the path starts with it (Starlette'sget_route_path, so/appand/api/v1/appboth match), part ofrequest.base_url. Refused together with WebSocket routes,app.mount()or Python-side routes.
What stays in Python¶
- Mounted applications (above), left to Python with
--python-side mount. - Raw ASGI routes: a class as endpoint (
HTTPEndpoint) is refused.
Differences¶
include_router: a runtime prefix containing a path parameter ({...}) stops the binary; a non-literal prefix inside a function (app factory) is refused at compile time;--python-sidecannot name a route under a runtime prefix (it is compiled).- Raw ASGI routes:
allowof a 405 lists the methods in declaration order (Starlette iterates a set, so its order varies between processes); mutations ofdependency_overridesare lost. - A 405 on an added
RoutelistsGET, HEADin that order (CPython: set order).