Declaring DTOs on app layers#
So far we’ve seen DTO declared per handler. Let’s have a look at a script that declares multiple handlers - something more typical of a real application.
1from __future__ import annotations
2
3from dataclasses import dataclass
4
5from litestar import Litestar, patch, post, put
6from litestar.dto import DataclassDTO, DTOConfig, DTOData
7from litestar.params import FromPath
8
9
10@dataclass
11class Person:
12 name: str
13 age: int
14 email: str
15 id: int
16
17
18class ReadDTO(DataclassDTO[Person]):
19 config = DTOConfig(exclude={"email"})
20
21
22class WriteDTO(DataclassDTO[Person]):
23 config = DTOConfig(exclude={"id"})
24
25
26class PatchDTO(DataclassDTO[Person]):
27 config = DTOConfig(exclude={"id"}, partial=True)
28
29
30@post("/person", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False)
31def create_person(data: DTOData[Person]) -> Person:
32 # Logic for persisting the person goes here
33 return data.create_instance(id=1)
34
35
36@put("/person/{person_id:int}", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False)
37def update_person(person_id: FromPath[int], data: DTOData[Person]) -> Person:
38 # Usually the Person would be retrieved from a database
39 person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com")
40 return data.update_instance(person)
41
42
43@patch("/person/{person_id:int}", dto=PatchDTO, return_dto=ReadDTO, sync_to_thread=False)
44def patch_person(person_id: FromPath[int], data: DTOData[Person]) -> Person:
45 # Usually the Person would be retrieved from a database
46 person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com")
47 return data.update_instance(person)
48
49
50app = Litestar(route_handlers=[create_person, update_person, patch_person])
DTOs can be defined on any layer of the application which gives us a chance to tidy up our code a bit. Let’s move the handlers into a controller and define the DTOs there.
1from __future__ import annotations
2
3from dataclasses import dataclass
4
5from litestar import Controller, Litestar, patch, post, put
6from litestar.dto import DataclassDTO, DTOConfig, DTOData
7from litestar.params import FromPath
8
9
10@dataclass
11class Person:
12 name: str
13 age: int
14 email: str
15 id: int
16
17
18class ReadDTO(DataclassDTO[Person]):
19 config = DTOConfig(exclude={"email"})
20
21
22class WriteDTO(DataclassDTO[Person]):
23 config = DTOConfig(exclude={"id"})
24
25
26class PatchDTO(DataclassDTO[Person]):
27 config = DTOConfig(exclude={"id"}, partial=True)
28
29
30class PersonController(Controller):
31 dto = WriteDTO
32 return_dto = ReadDTO
33
34 @post("/person", sync_to_thread=False)
35 def create_person(self, data: DTOData[Person]) -> Person:
36 # Logic for persisting the person goes here
37 return data.create_instance(id=1)
38
39 @put("/person/{person_id:int}", sync_to_thread=False)
40 def update_person(self, person_id: FromPath[int], data: DTOData[Person]) -> Person:
41 # Usually the Person would be retrieved from a database
42 person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com")
43 return data.update_instance(person)
44
45 @patch("/person/{person_id:int}", dto=PatchDTO, sync_to_thread=False)
46 def patch_person(self, person_id: FromPath[int], data: DTOData[Person]) -> Person:
47 # Usually the Person would be retrieved from a database
48 person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com")
49 return data.update_instance(person)
50
51
52app = Litestar(route_handlers=[PersonController])
The previous script had separate handler functions for each route, whereas the new script organizes these into a
PersonController class, allowing us to move common configuration to the controller layer.
We have defined both dto=WriteDTO and return_dto=ReadDTO on the PersonController class, removing the need
to define these on each handler. We still define PatchDTO directly on the patch_person handler, to override the
controller level dto setting for that handler.