The Controller Layer
This section explores the creation of the HauntedHousesController class, a pivotal component in
any pest API. The controller is responsible for handling incoming HTTP requests and generating
appropriate responses. It defines the routes and actions necessary for managing haunted houses.
HauntedHousesController class
When we first introduced the Hounted House API, we mentioned that our API would respond to HTTP requests for managing haunted houses CRUD operations (create, read, update, delete).
The HauntedHousesController class is where we define these routes and actions.
A Controller is a class decorated with the @controller decorator.
The decorator takes at least one argument: the route prefix. This prefix will be prepended to all the routes defined in the controller.
Each route is defined by a method, also known as a request handler, decorated with one of the HTTP
method decorators: @get, @post, @put, @delete provided by pest.
Now, let's create the HauntedHousesController class in the haunted_houses_controller.py file and
implement our first request handler: a get_houses method to handle GET /haunted-houses requests.
from pest import controller, get, post, put, delete
from .haunted_houses_service import HauntedHousesService
from .repository import HauntedHouse
@controller("/haunted-houses")
class HauntedHousesController:
"""Haunted-houses routes"""
service: HauntedHousesService # 💉 injected
@get("/")
def get_houses(self) -> list[HauntedHouse]:
"""Get all haunted houses"""
return self.service.get_houses()A lot is going on in this small code snippet, so let's break it down.
@controller("/haunted-houses")
class HauntedHousesController:By decorating the HauntedHousesController class with the @controller decorator, we are telling
pest that this class serves as a controller, and all the routes defined within it will carry
the prefix /haunted-houses.
service: HauntedHousesService # 💉 injectedWithin the HauntedHousesController class, there's a service service attribute of type
HauntedHousesService. It's important to note that we're not creating an instance of the service
class here; instead, we're declaring it as a class attribute. When pest initializes the
controller, it will automatically instantiate the required service and inject it into the
controller.
@get("/")
def get_houses(self) -> list[HauntedHouse]:
"""Get all haunted houses"""
return self.service.get_houses()-
The
get_housesmethod is decorated with the@get("/")decorator, indicating pest that this method will handleGETrequests to the root/haunted-housesroute. -
The docstring of the method will be used as the description of the route in the OpenAPI schema.
-
The method returns a list of
HauntedHouseobjects; but sinceHauntedHouseis a pydantic model, and pest is built on top of FastAPI, the response is automatically serialized to JSON. -
Typically, we keep controller methods simple, focusing on transport-layer logic like parsing the request and validating it. Business logic is often delegated to a service class. In this case, we call the
get_housesmethod of theHauntedHousesServiceclass and return the result. While not mandatory, it's a good practice to maintain simplicity in controller methods.Controllers ideally handle transport-layer-related tasks such as request parsing, validation, and data transformation. pest and FastAPI streamline many of these processes before reaching our handler and after the handler returns.
Let's test it!
Before proceeding with the implementation of the rest of the controller, it's time to test what we have so far.
In order to do so, we need to revisit the HauntedHousesModule class again, and include the
controller class in the controllers list. This step ensures that the controller is registered
within the module, allowing pest to discover it.
from pest import module
from .haunted_houses_controller import HauntedHousesController
from .haunted_houses_service import HauntedHousesService
from .repository import HauntedHousesRepository
@module(
controllers=[HauntedHousesController],
providers=[HauntedHousesService, HauntedHousesRepository],
)
class HauntedHousesModule:
passWith the controller now registered, it's time to test our route. Start the server and make a request
to the /haunted-houses route.
Open your browser or use a tool like curl, postman, etc. to send a GET request:
If everything went well, you should see the list of our haunted houses in the response as a JSON array. Hurrah! We have our first route up and running!
Define the rest of the request handlers (or routes)
Now that we know how to define a route, let's define the rest of the routes for managing all the CRUD operations for haunted houses.
Get a haunted house by id
from pest import controller, get, post, put, delete
from .haunted_houses_service import HauntedHousesService
from .repository import HauntedHouse
@controller("/haunted-houses")
class HauntedHousesController:
"""Haunted-houses routes"""
service: HauntedHousesService # 💉 injected
...
@get("/{house_id}")
def get_house_by_id(self, house_id: int) -> HauntedHouse:
"""Get a haunted house by ID"""
return self.service.get_house_by_id(house_id)Similar to the previous route, the only difference this time is the addition of a path parameter:
/{house_id}. This parameter will be passed as an argument to the get_house_by_id method.
For instance, when the route /haunted-houses/1 is requested, the get_house_by_id method will
receive a house_id argument with the value 1. Using our service instance, we retrieve the
haunted house by its ID and return it.
Let's test it!
Create a haunted house
When it comes to creating entities, the POST HTTP method is commonly used. Let's create a route
and a request handler for creating haunted houses.
from pest import controller, get, post, put, delete
from .haunted_houses_service import HauntedHousesService
from .repository import HauntedHouse
@controller("/haunted-houses")
class HauntedHousesController:
"""Haunted-houses routes"""
service: HauntedHousesService # 💉 injected
...
@post("/")
def create_house(self, house: HauntedHouse) -> HauntedHouse:
"""Create a new haunted house"""
return self.service.create_house(house)In order to handle POST requests, we use the @post decorator. The create_house method takes a
house argument of type HauntedHouse and returns a HauntedHouse object.
The HauntedHouse object is automatically deserialized from the request body, validated, and
converted to a Python object, thanks to the power of Pydantic and FastAPI. We then delegate
the actual creation of the haunted house to the previously defined service and return the result.
Time to give it a try!
Nice! Apparently, our haunted house was successfully created. Let's check if it's in the list of
haunted houses by making a GET request to the /haunted-houses route.
Oh no! It looks like our new haunted house was not saved 😢
What's going on here?
By default, pest registers all providers with a transient scope. This means that a new instance of the provider is created every time it's injected into a controller or another provider.
That's the case of our HauntedHousesRepository class. Every time the HauntedHousesService class
is instantiated, a new instance of the HauntedHousesRepository class is created and injected into
the service. This behavior might be desirable in most cases, but not in this one because we want to
keep the list of haunted houses in memory.
Let's change the scope of the HauntedHousesRepository class to a singleton. This way, only one
instance of the repository will be created and shared across the application.
from pest import module
from pest.metadata.types.module_meta import ValueProvider
from .haunted_houses_controller import HauntedHousesController
from .haunted_houses_service import HauntedHousesService
from .repository import HauntedHousesRepository
@module(
controllers=[HauntedHousesController],
providers=[
HauntedHousesService,
ValueProvider(
provide=HauntedHousesRepository,
use_value=HauntedHousesRepository(),
),
],
)
class HauntedHousesModule:
passNow, instead of registering the HauntedHousesRepository class, we register a ValueProvider with
a specific instance of the repository. This way, the same instance of the repository will be shared
across the application.
Now if we create our haunted house again and then try to retrieve the list of haunted houses, we should see our new haunted house in the list:
Beautiful! 🍾
Update a haunted house
When it comes to updating entities, we utilize the PUT HTTP method. Let's create a route for
updating haunted houses.
from pest import controller, get, post, put, delete
from .haunted_houses_service import HauntedHousesService
from .repository import HauntedHouse
@controller("/haunted-houses")
class HauntedHousesController:
"""Haunted-houses routes"""
service: HauntedHousesService # 💉 injected
...
@put("/{house_id}")
def update_house(self, house_id: int, updated_house: HauntedHouse) -> HauntedHouse:
"""Update an existing haunted house"""
return self.service.update_house(house_id, updated_house)Here, we define a route for updating haunted houses using the @put decorator. The update_house
method expects two arguments: house_id as a path parameter, and updated_house, which is
deserialized from the request body.
If you come from FastAPI, you might find this quite familiar. pest is built on top of FastAPI, so it inherits many of its features. Importantly, pest request handlers are, essentially, FastAPI routes in the background. This means that everything you can do with FastAPI, you can seamlessly achieve with pest handlers. For example, you can leverage FastAPI's powerful features for handling body parameters, request validation, and more. Refer to the FastAPI documentation (opens in a new tab) for detailed insights on utilizing these capabilities.
The business logic for the update is delegated to the service, and the result is returned. As before, pest and FastAPI handle tasks like validation and serialization seamlessly.
Apparently, we found 2 more ghosts in our Spooky Manor haunted house. Let's update it!
Delete a haunted house
Finally, we need a route for deleting haunted houses (once all the ghosts are gone, of course 😛).
We use the DELETE HTTP method for that.
from pest import controller, get, post, put, delete
from .haunted_houses_service import HauntedHousesService
from .repository import HauntedHouse
@controller("/haunted-houses")
class HauntedHousesController:
"""Haunted-houses routes"""
service: HauntedHousesService # 💉 injected
...
@delete("/{house_id}")
def delete_house(self, house_id: int) -> None:
"""Delete a haunted house by ID"""
self.service.delete_house(house_id)If we send a DELETE request to the /haunted-houses/1 route, the delete_house method will be
called with the house_id argument set to 1. The service will then communicate with the
repository to delete the haunted house.
Let's try it out!
If we try to retrieve the list of haunted houses again, we should see that the Spooky Manor is
gone with all its ghosts 👻
There we go! We have all the routes we need to manage haunted houses.
The entire controller
At this point, our controller should look like this.
from pest import controller, get, post, put, delete
from .haunted_houses_service import HauntedHousesService
from .repository import HauntedHouse
@controller("/haunted-houses")
class HauntedHousesController:
"""Haunted-houses routes"""
service: HauntedHousesService # 💉 injected
@get("/")
def get_houses(self) -> list[HauntedHouse]:
"""Get all haunted houses"""
return self.service.get_houses()
@get("/{house_id}")
def get_house_by_id(self, house_id: int) -> HauntedHouse:
"""Get a haunted house by ID"""
return self.service.get_house_by_id(house_id)
@post("/")
def create_house(self, house: HauntedHouse) -> HauntedHouse:
"""Create a new haunted house"""
return self.service.create_house(house)
@put("/{house_id}")
def update_house(self, house_id: int, updated_house: HauntedHouse) -> HauntedHouse:
"""Update an existing haunted house"""
return self.service.update_house(house_id, updated_house)
@delete("/{house_id}")
def delete_house(self, house_id: int) -> None:
"""Delete a haunted house by ID"""
self.service.delete_house(house_id)All the CRUD operations for haunted houses are now implemented. Yay! 🎉
Conclusion
Across this course, we've explored the core concepts of creating APIs with the pest framework.
From crafting controller classes to defining routes and handling HTTP requests, we've covered the
essential aspects of API development with pest.
Our exploration included insights into dependency injection and the importance of delegating business logic to a service. These practices not only enhance modularity but also contribute to the maintainability of our API.
In the upcoming courses, we'll dive into more advanced features like authentication, authorization, testing, and more! Stay tuned!