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
7
8
9@dataclass
10class Person:
11 name: str
12 age: int
13 email: str
14 id: int
15
16
17class ReadDTO(DataclassDTO[Person]):
18 config = DTOConfig(exclude={"email"})
19
20
21class WriteDTO(DataclassDTO[Person]):
22 config = DTOConfig(exclude={"id"})
23
24
25class PatchDTO(DataclassDTO[Person]):
26 config = DTOConfig(exclude={"id"}, partial=True)
27
28
29@post("/person", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False)
30def create_person(data: DTOData[Person]) -> Person:
31 # Logic for persisting the person goes here
32 return data.create_instance(id=1)
33
34
35@put("/person/{person_id:int}", dto=WriteDTO, return_dto=ReadDTO, sync_to_thread=False)
36def update_person(person_id: int, data: DTOData[Person]) -> Person:
37 # Usually the Person would be retrieved from a database
38 person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com")
39 return data.update_instance(person)
40
41
42@patch("/person/{person_id:int}", dto=PatchDTO, return_dto=ReadDTO, sync_to_thread=False)
43def patch_person(person_id: int, data: DTOData[Person]) -> Person:
44 # Usually the Person would be retrieved from a database
45 person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com")
46 return data.update_instance(person)
47
48
49app = 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
7
8
9@dataclass
10class Person:
11 name: str
12 age: int
13 email: str
14 id: int
15
16
17class ReadDTO(DataclassDTO[Person]):
18 config = DTOConfig(exclude={"email"})
19
20
21class WriteDTO(DataclassDTO[Person]):
22 config = DTOConfig(exclude={"id"})
23
24
25class PatchDTO(DataclassDTO[Person]):
26 config = DTOConfig(exclude={"id"}, partial=True)
27
28
29class PersonController(Controller):
30 dto = WriteDTO
31 return_dto = ReadDTO
32
33 @post("/person", sync_to_thread=False)
34 def create_person(self, data: DTOData[Person]) -> Person:
35 # Logic for persisting the person goes here
36 return data.create_instance(id=1)
37
38 @put("/person/{person_id:int}", sync_to_thread=False)
39 def update_person(self, person_id: int, data: DTOData[Person]) -> Person:
40 # Usually the Person would be retrieved from a database
41 person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com")
42 return data.update_instance(person)
43
44 @patch("/person/{person_id:int}", dto=PatchDTO, sync_to_thread=False)
45 def patch_person(self, person_id: int, data: DTOData[Person]) -> Person:
46 # Usually the Person would be retrieved from a database
47 person = Person(id=person_id, name="John", age=50, email="email_of_john@example.com")
48 return data.update_instance(person)
49
50
51app = 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.