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 # 💉 injected
Within 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_houses
method is decorated with the@get("/")
decorator, indicating pest that this method will handleGET
requests to the root/haunted-houses
route. -
The docstring of the method will be used as the description of the route in the OpenAPI schema.
-
The method returns a list of
HauntedHouse
objects; but sinceHauntedHouse
is 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_houses
method of theHauntedHousesService
class 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:
pass
With 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:
pass
Now, 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!