Skip to content

Path parameters

Path parameters are declared in the route definition, using the same format as formatting strings ("{}"): To tell Xpresso that you want to extract a path parameter and inject it into your function, use FromPath. This is just a marker that tells Xpresso how to inject the value, it has no effect on the function if it is called directly.

from typing import Dict

from xpresso import App, FromPath, Path


async def read_item(item_id: FromPath[str]) -> Dict[str, str]:
    return {"item_id": item_id}


app = App(
    routes=[
        Path(
            path="/items/{item_id}",
            get=read_item,
        ),
    ]
)

If you run this example using Uvicorn, you can go to http://127.0.0.1:8000/items/1234 (1234 is an arbitrary string) and you'll get a JSON response in your browser like:

{
  "item_id": "1234"
}

Type conversions

Xpresso uses type annotations from your parameters to do conversions and parsing. If we modify the example above to expect an int, Xpresso will convert the path parameter (which is always a string, since it is coming from a URL) into an int and automatically return an error response if it is not a valid integer:

from typing import Dict

from xpresso import App, FromPath, Path


async def read_item(item_id: FromPath[int]) -> Dict[str, int]:
    return {"item_id": item_id}


app = App(
    routes=[
        Path(
            path="/items/{item_id}",
            get=read_item,
        ),
    ]
)

Now if you navigate to http://127.0.0.1:8000/items/1234 (the same URL as before) you will get back a response with an integer instead of a string:

{
  "item_id": 1234
}

Let's try passing in something that is not an integer. Navigate to http://127.0.0.1:8000/items/foobarbaz and you'll get back:

{
  "detail": [
    {
      "loc": [
        "path",
        "item_id"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ]
}

Under the hood, we use Pydantic for this parsing and validation. So you can also use Pydantic constraints:

from typing import Dict

from pydantic import Field

from xpresso import App, Path, PathParam
from xpresso.typing import Annotated


async def read_item(
    item_id: Annotated[int, PathParam(), Field(gt=0)]
) -> Dict[str, int]:
    return {"item_id": item_id}


app = App(
    routes=[
        Path(
            path="/items/{item_id}",
            get=read_item,
        ),
    ]
)

Info

This is probably a good spot to digress and talk about Annotated since you may be confused if you are not familiar with it. If you've used FastAPI, you may be used to declaring things like param: str = Path(gt=0). In Xpresso, this turns into param: Annotated[str, Path(), Field(gt=0)]. For more background on Annotated itself, see the Python Types section of our docs.

Tip

The import from Xpresso.typing import Annotated is just a convenience import. All it does is import Annotated from typing if your Python version is >= 3.9 and typing_extensions otherwise. But if you are already using Python >= 3.9, you can just replace that with from typing import Annotated.

Navigating to http://127.0.0.1:8000/items/1 (a positive value) will give you:

{
  "item_id": 1
}

But if you try http://127.0.0.1:8000/items/-1 (a negative value):

{
  "detail": [
    {
      "ctx": {
        "limit_value": 0
      },
      "loc": [
        "path",
        "item_id"
      ],
      "msg": "ensure this value is greater than 0",
      "type": "value_error.number.not_gt"
    }
  ]
}

OpenAPI documentation

If you run one of the examples above and navigate to http://127.0.0.1:8000/docs you'll see the OpenAPI documentation. It should look something like:

Swagger UI

Array path parameters

Xpresso offers full support for the the OpenAPI parameter serialization spec. You can control the serialization style using the style and explode arguments to PathParam(): The Python type can be a scalar value, a collection (like a list or dict) or even a Pydantic model (for object-valued parameters).

from typing import List

from xpresso import App, Path, PathParam
from xpresso.typing import Annotated


async def read_items(
    items: Annotated[
        List[int], PathParam(explode=True, style="matrix")
    ]
) -> List[int]:
    return items


app = App(
    routes=[
        Path(
            path="/items/{items}",
            get=read_items,
        ),
    ]
)

Navigating to http://127.0.0.1:8000/items/;items=1;items=2;items=3 will return the following JSON:

[
  1,
  2,
  3
]