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.