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.