Skip to content

Routing

Xpresso has a simple routing system, based on Starlette's routing system and the OpenAPI spec.

There are 4 main players in Xpresso's routing system:

  • Operation: this is equivalent to an OpenAPI operation. An operation is a unique combination of an HTTP method and a path, and has a 1:1 relationship with endpoint functions. Xpresso's Operation class is derived from a Starlette BaseRoute.
  • Path: this is the equivalent of an OpenAPI PathItem. A Path can contain 1 Operation for each method (but does not have to). Paths are were you specify your actual path like /items/{item_id}. Path is derived from Starlette's Route.
  • Router: similar to Starlette's router but only supporting adding routes at initialization (@router.route does not exist in Xpresso). Additionally, it supports adding router-level dependencies, middleware and OpenAPI tags.
  • App: this is the top-level application object where you configure your dependency injection container and OpenAPI docs. Functionality is similar to Starlette's Starlette application, but just like Router the dynamic methods like add_middleware() and add_exception_handler() are not supported. App also accepts middleware and dependencies, but these are just passed though to its router (App.router).

All of these are meant to work with Starlette, so you can mount a Starlette application into Xpresso as a route using Mount, or use a Starlette router in the middle of Xpresso's routing system.

from xpresso import App, Operation, Path, Router
from xpresso.routing.mount import Mount


async def items() -> None:
    ...


path = Path("/items", get=Operation(items))

inner_mount = Mount(path="/mount-again", routes=[path])

router = Router(routes=[inner_mount])

outer_mount = Mount(path="/mount", app=router)

app = App(routes=[outer_mount])

See Starlette's routing docs for more general information on Starlette's routing system.

Customizing OpenAPI schemas for Operation and Path

Operation, Path and Router let you customize their OpenAPI schema. You can add descriptions, tags and detailed response information:

  • Add tags via the tags parameter
  • Exclude a specific Operation from the schema via the include_in_schema parameter
  • Add a summary for the Operation via the summary parameter
  • Add a description for the Operation via the description parameter (by default the endpoint function's docstring)
  • Mark the operation as deprecated via the deprecated parameter
  • Customize responses via the responses parameter
from typing import List, Mapping

from pydantic import BaseModel

from xpresso import App, FromJson, Operation, Path, Response, Router
from xpresso.openapi.models import Server
from xpresso.responses import ResponseSpec
from xpresso.routing.mount import Mount


class Item(BaseModel):
    name: str
    price: float


fake_items_db = {
    "chair": Item(name="chair", price=30.29),
    "hammer": Item(name="hammer", price=1.99),
}


async def get_items() -> Mapping[str, Item]:
    """Docstring will be ignored"""
    return fake_items_db


async def create_item(item: FromJson[Item]) -> Response:
    """Documentation from docstrings!
    You can use any valid markdown, for example lists:

    - Point 1
    - Point 2
    """
    fake_items_db[item.name] = item
    return Response(status_code=204)


async def delete_items(items_to_delete: FromJson[List[str]]) -> None:
    for item_name in items_to_delete:
        fake_items_db.pop(item_name, None)


items = Path(
    "/items",
    get=Operation(
        get_items,
        description="The **items** operation",
        summary="List all items",
        deprecated=True,
        tags=["read"],
    ),
    post=Operation(
        create_item,
        responses={"204": ResponseSpec(description="Success")},
        servers=[
            Server(url="https://us-east-1.example.com"),
            Server(url="http://127.0.0.1:8000"),
        ],
        tags=["write"],
    ),
    delete=Operation(
        delete_items,
        include_in_schema=False,
    ),
    include_in_schema=True,  # the default
    servers=[Server(url="http://127.0.0.1:8000")],
    tags=["items"],
)

app = App(
    routes=[
        Mount(
            path="/v1",
            app=Router(
                routes=[items],
                responses={
                    "404": ResponseSpec(description="Item not found")
                },
                tags=["v1"],
            ),
        )
    ]
)

This will look something like this:

Swagger UI

Note

Tags and responses accumulate. Responses are overwritten with the lower level of the routing tree tacking precedence, so setting the same status code on a Router and Operation will result in the Operation's version overwriting Router's. The servers array completely overwrites any parents: setting servers on Operation will overwrite all servers set on Routers or Path.