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.
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:
Using a dataclass also gives you better validation: omitting a key such as title
will result in a useful error response:
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