Skip to content

Controllers

The Controller plays a crucial role in managing incoming requests and providing responses to clients. Its primary function is to handle specific requests directed to an application's ApplicationRouter. In turn, the ApplicationRouter determines the appropriate controller to manage the incoming request.

controller description image

Conceptually, controllers can be likened to routers with multiple registered routes within them.

Creating a Controller

To create a controller, we use classes and decorators. The Controller decorator associates classes with a required metadata needed for Ellar to create a routing table

from ellar.common import Controller, ControllerBase

@Controller()
class UserController(ControllerBase):
    """We have created a controller that will manage our Users"""

Routing

In this section, we will outline the key features of @Controller(), a class decorator designed for defining a controller. By default, when applied, @Controller() generates a path prefix based on the class name, such as /car for a controller named CarController. This feature aims to organize and group related routes, reducing redundancy in route definitions.

For instance, if we want to group a collection of routes managing interactions with a customer entity under the route /user, we can specify the path prefix /user in the @Controller() decorator. This ensures that we don't need to repeat this portion of the path for each route within the controller.

# project_name/apps/car/controllers.py

from ellar.common import Controller, get, ControllerBase

@Controller('/car')
class CarController(ControllerBase):
    @get()
    def get_all(self):
        return 'This action returns all car'

    @get("/welcome")
    def index(self):
        return {"detail": "Welcome to Car Resource"}

Hint

Class decorators are conventionally named with capital letters, while function/method decorator names typically use lowercase letters.

The @get() HTTP method decorator preceding the get_all(self) method designates get_all(self) as the HTTP request handler responsible for handling a specific endpoint matching the route path and HTTP method of GET.

But what exactly is the route path for get_all(self)? The route path is determined by combining the controller's path prefix and the path specified in the HTTP method function decorator @get().

For instance, if we've set a prefix for every route (car) and haven't added any path information in the decorator, it means the path defaults to /. In this case, Ellar will associate GET /car/ requests with the get_all(self) handler.

To illustrate further, if we have a path prefix of /users and include the decorator @get('/profile'), it would result in a route mapping for requests like GET /users/profile.

Overview of HTTP function decorator parameters:

@get(path: str, name: str, include_in_schema: bool, response: t.Union[t.Dict[int, t.Type], t.List[t.Tuple[int, t.Type]], t.Type])

  • path: defines the path for route mapping. default='/'
  • name: defines a name that will be used to identify this route during URL reversing. default is function name eg: get_all
  • include_in_schema: indicates if an endpoint should be available in OPENAPI docs
  • response: defines different response schema, status code, and Response type that is available on the endpoint. This is only necessary for OPENAPI documentation. default: None

Ellar serializes all route handler returned data to JSON unless the data returned is a Response object. For the above example, get_all returned a string. This will be serialized to JSON with a status code 200.

Request Object

There are different ways handlers can access client request details:

Annotation (parameter_name:Request)

Ellar will resolve any parameter annotated as Request in the request handler signature as a Request object.

# project_name/apps/car/controllers.py

from ellar.common import Controller, get, ControllerBase
from ellar.core import Request

@Controller('/car')
class CarController(ControllerBase):
    @get()
    def get_all(self, request: Request):
        assert isinstance(request, Request) # True
        return 'This action returns all cars'

    ...

injection (parameter_name=Inject[Request])

We can also inject request object to any handler by using Inject[Request] decorator in handler signature.

# project_name/apps/car/controllers.py

from ellar.common import Controller, ControllerBase, get, Inject
from ellar.core import Request

@Controller('/car')
class CarController(ControllerBase):
    @get()
    def get_all(self, req_data: Request, req_data_2: Inject[Request]):
        assert isinstance(req_data, Request) # True
        assert isinstance(req_data_2, Request)
        assert req_data == req_data_2
        return 'This action returns all cars'
    ...

Controllers Context

During request handler execution, Execution Context is available on the Controller instance and the request object can be gotten from the context.

# project_name/apps/car/controllers.py

from ellar.common import Controller, ControllerBase, get
from ellar.core import Request

@Controller('/car')
class CarController(ControllerBase):
    @get()
    def get_all(self):
        assert isinstance(self.context.switch_to_http_connection().get_request(), Request) # True
        return 'This action returns all cars'
    ...

Other request handler signature injectors

Inject[Request] for Request object
Inject[Response] for Response object
Path() pydantic field - resolves path parameters
Body() pydantic field - resolves required Request body parameters
Form() pydantic field - resolves required Request body parameters with content-type=x-www-form-urlencoded
Header() pydantic field - resolves required Request header parameters
Query() pydantic field - resolves required Request query parameters
File() pydantic field - resolves required Request body parameters with content-type=x-www-form-urlencoded
Inject[ExecutionContext] Injects ExecutionContext.
Cookie() pydantic field - resolves required Request cookie parameters
Inject[dict, Inject.Key('Session')] injects Request session data
Inject[str, Inject.Key('Host')] injects Request clients host
Inject[Type] injects services

Resource

Let add create endpoint to our CarController resource.

# project_name/apps/car/controllers.py

from ellar.common import Controller, ControllerBase, get, post
from ellar.core import Request

@Controller('/car')
class CarController(ControllerBase):
    @post()
    def create(self):
        return 'This action adds a new car'

    @get()
    def get_all(self):
        assert isinstance(self.context.switch_to_http_connection().get_request(), Request) # True
        return 'This action returns all cars'
    ...

HTTP Methods

Ellar provides decorators for all the standard HTTP methods:

  • @get - GET HTTP method
  • @post - POST HTTP method
  • @put - PUT HTTP method
  • @patch - PATCH HTTP method
  • @delete - DELETE HTTP method
  • @trace - TRACE HTTP method
  • @options - OPTIONS HTTP method
  • @head - HEAD HTTP method
  • @http_route - allows one or more HTTP methods combination, eg: @http_route(methods=['PUT', 'PATCH'])

Asynchronicity

Ellar supports modern asynchronous programming in python using async and await syntax.

# project_name/apps/car/controllers.py

from ellar.common import Controller, ControllerBase, get, post
from ellar.core import Request

@Controller('/car')
class CarController(ControllerBase):
    @post()
    async def create(self):
        return 'This action adds a new car'

    @get()
    async def get_all(self):
        assert isinstance(self.context.switch_to_http_connection().get_request(), Request) # True
        return 'This action returns all cars'
    ...

Request Payload

Let's use @Body() to define the required data to create a car in our previous create(POST) endpoint. Before that, we need to define our data input/output serializers

# project_name/apps/car/schema.py
from ellar.common import Serializer
from pydantic import Field


class CreateCarSerializer(Serializer):
    name: str
    year: int = Field(..., gt=0)
    model: str


class CarListFilter(Serializer):
    offset: int = 1
    limit: int = 10


class CarSerializer(Serializer):
    id: str
    name: str
    year: int
    model: str

Let's add the CreateCarSerializer to create endpoint,

# project_name/apps/car/controllers.py
...
from ellar.common import Body, post
from .schemas import CreateCarSerializer


@post()
async def create(self, payload: Body[CreateCarSerializer]):
    return 'This action adds a new car'

CreateCarSerializer is a Pydantic type, which implies that the name, year, and model fields undergo automatic type validation.

It's crucial to observe how we utilized CreateCarSerializer as a type annotation for the payload parameter in the create route handler method. Ellar will calculate values for all route handler parameters and validate them according to the annotated types before executing the handler.

Info

if a parameter is not annotated, it will be assumed as a string type

CreateCarSchema

Let's add other endpoints

# project_name/apps/car/controllers.py

from ellar.common import Body, Controller, ControllerBase, delete, get, post, put, Query
from ellar.core import Request
from .schemas import CreateCarSerializer, CarListFilter


@Controller('/car')
class CarController(ControllerBase):
    @post()
    async def create(self, payload:Body[CreateCarSerializer]):
        result = payload.dict()
        result.update(message='This action adds a new car')
        return result

    @put('/{car_id:str}')
    async def update(self, car_id: str, payload:Body[CreateCarSerializer]):
        result = payload.dict()
        result.update(message=f'This action updated #{car_id} car resource')
        return result

    @get('/{car_id:str}')
    async def get_one(self, car_id: str):
        return f"This action returns a #{car_id} car"

    @delete('/{car_id:str}')
    async def delete(self, car_id: str):
        return f"This action removes a #{car_id} car"

    @get()
    async def get_all(self, query:Query[CarListFilter]):
        assert isinstance(self.context.switch_to_http_connection().get_request(), Request)  # True
        return f'This action returns all cars at limit={query.limit}, offset={query.offset}'

Linking Controller

On the preceding page, we successfully connected our car module (CarModule) to the ApplicationModule in project_name/root_module. However, this time, we will be introducing modifications to the CarModule to keep things simple, organized and modular.

To achieve this, let's include the registration of CarController within the CarModule. The @Module() decorator accepts a controllers parameter, which is an array of the ControllerBase type.

In the car/module.py,

# project_name/apps/car/module.py

from ellar.common import Module
from ellar.core import ModuleBase
from ellar.di import Container

from .controllers import CarController


@Module(
    controllers=[CarController],
    providers=[],
    routers=[],
)
class CarModule(ModuleBase):
    pass

controller_dog.gif