Making the list interactive#

So far, our TODO list application is not very useful, since it’s static. You can’t update items, nor add or remove them.

Receiving incoming data#

Let’s start by implementing a route handler that handles the creation of new items. In the previous step you used the get decorator, which responds to the GET HTTP method. In this case we want to react to POST requests, so we are going to use the corresponding post decorator.

 1from typing import Any, Dict, List, Union
 2
 3from litestar import Litestar, post
 4
 5TODO_LIST: List[Dict[str, Union[str, bool]]] = []
 6
 7
 8@post("/")
 9async def add_item(data: Dict[str, Any]) -> List[Dict[str, Union[str, bool]]]:
10    TODO_LIST.append(data)
11    return TODO_LIST
12
13
14app = Litestar([add_item])
 1from typing import Any, Union
 2
 3from litestar import Litestar, post
 4
 5TODO_LIST: list[dict[str, Union[str, bool]]] = []
 6
 7
 8@post("/")
 9async def add_item(data: dict[str, Any]) -> list[dict[str, Union[str, bool]]]:
10    TODO_LIST.append(data)
11    return TODO_LIST
12
13
14app = Litestar([add_item])
 1from typing import Any
 2
 3from litestar import Litestar, post
 4
 5TODO_LIST: list[dict[str, str | bool]] = []
 6
 7
 8@post("/")
 9async def add_item(data: dict[str, Any]) -> list[dict[str, str | bool]]:
10    TODO_LIST.append(data)
11    return TODO_LIST
12
13
14app = Litestar([add_item])

Request data can be received via the data keyword. Litestar will recognize this, and supply the data being sent with the request via this parameter. As with the query parameters in the previous chapter, we use the type annotations to configure what type of data we expect to receive, and set up validation. In this case, Litestar will expect request data in the form of JSON and use the type annotation we gave it to convert it into the correct format.

See also

Using the interactive documentation to test a route#

Since our example now uses the POST HTTP method, you can no longer simply visit the URL in our browser and get a response. Instead, you can use the interactive documentation to send a POST request. Because of the OpenAPI schema generated by Litestar, Swagger will know exactly what kind of data to send. In this example, it will send a simple JSON object.

../../_images/swagger-post-dict-response.png

Sending a sample request to our add_item route reveals a successful response#

Improving the example with dataclasses#

As in the previous chapter, this too can be improved by using dataclasses instead of plain dicts.

 1from dataclasses import dataclass
 2from typing import List
 3
 4from litestar import Litestar, post
 5
 6
 7@dataclass
 8class TodoItem:
 9    title: str
10    done: bool
11
12
13TODO_LIST: List[TodoItem] = []
14
15
16@post("/")
17async def add_item(data: TodoItem) -> List[TodoItem]:
18    TODO_LIST.append(data)
19    return TODO_LIST
20
21
22app = Litestar([add_item])
 1from dataclasses import dataclass
 2
 3from litestar import Litestar, post
 4
 5
 6@dataclass
 7class TodoItem:
 8    title: str
 9    done: bool
10
11
12TODO_LIST: list[TodoItem] = []
13
14
15@post("/")
16async def add_item(data: TodoItem) -> list[TodoItem]:
17    TODO_LIST.append(data)
18    return TODO_LIST
19
20
21app = Litestar([add_item])

This is not only easier on the eyes and adds more structure to the code, but also gives better interactive documentation; it will now present us with the field names and default values for the dataclass we have defined:

../../_images/swagger-dict-vs-dataclass.png

Documentation for the add_item route with data typed as a dict vs dataclass#

Using a dataclass also gives you better validation: omitting a key such as title will result in a useful error response:

../../_images/swagger-dataclass-bad-body.png

Sending a request without a title key fails#

Create dynamic routes using path parameters#

The next task on the list is updating an item’s status. For this, a way to refer to a specific item on the list is needed. This could be done using query parameters, but there’s an easier, and more semantically coherent way of expressing this: path parameters.

@get("/{name:str}")
async def greeter(name: str) -> str:
    return "Hello, " + name

So far all the paths in your application are static, meaning they are expressed by a constant string which does not change. In fact, the only path used so far is /.

Path parameters allow you to construct dynamic paths and later refer to the dynamically captured parts. This may sound complex at first, but it’s actually quite simple; you can think of it as a regular expression that’s being used on the requested path.

Path parameters consist of two parts: an expression inside the path, describing the parameter, and a corresponding function parameter of the same name in the route handler function, which will receive the path parameter’s value.

In the above example, a path parameter name:str is declared, which means that now, a request to the path /john can be made, and the greeter function will be called as greeter(name="john"), similar to how query parameters are injected.

Tip

Just like query parameters, path parameters can convert and validate their values as well. This is configured using the :type colon annotation, similar to type annotations. For example, value:str will receive values as a string, while value:int will try to convert it into an integer.

A full list of supported types can be found here: Supported Path Parameter Types

By using this pattern and combining it with those from the earlier section about receiving data you can now set up a route handler that takes in the title of a TODO item, an updated item in form of a dataclass instance, and updates the item in the list.

from dataclasses import dataclass
from typing import List

from litestar import Litestar, put
from litestar.exceptions import NotFoundException


@dataclass
class TodoItem:
    title: str
    done: bool


TODO_LIST: List[TodoItem] = [
    TodoItem(title="Start writing TODO list", done=True),
    TodoItem(title="???", done=False),
    TodoItem(title="Profit", done=False),
]


def get_todo_by_title(todo_name) -> TodoItem:
    for item in TODO_LIST:
        if item.title == todo_name:
            return item
    raise NotFoundException(detail=f"TODO {todo_name!r} not found")


@put("/{item_title:str}")
async def update_item(item_title: str, data: TodoItem) -> List[TodoItem]:
    todo_item = get_todo_by_title(item_title)
    todo_item.title = data.title
    todo_item.done = data.done
    return TODO_LIST


app = Litestar([update_item])
from dataclasses import dataclass

from litestar import Litestar, put
from litestar.exceptions import NotFoundException


@dataclass
class TodoItem:
    title: str
    done: bool


TODO_LIST: list[TodoItem] = [
    TodoItem(title="Start writing TODO list", done=True),
    TodoItem(title="???", done=False),
    TodoItem(title="Profit", done=False),
]


def get_todo_by_title(todo_name) -> TodoItem:
    for item in TODO_LIST:
        if item.title == todo_name:
            return item
    raise NotFoundException(detail=f"TODO {todo_name!r} not found")


@put("/{item_title:str}")
async def update_item(item_title: str, data: TodoItem) -> list[TodoItem]:
    todo_item = get_todo_by_title(item_title)
    todo_item.title = data.title
    todo_item.done = data.done
    return TODO_LIST


app = Litestar([update_item])

See also