Skip to content

APIView

This is a special object from Esmerald and aims to implement the so needed class based views for those who love object oriented programming. Inspired by such great frameworks (Python, Go, JS), APIView was created to simplify the life of those who like OOP.

APIView class

from esmerald.permissions import DenyAll, IsAuthenticated
from esmerald.requests import Request
from esmerald.responses import JSONResponse
from esmerald.routing.apis.views import APIView
from esmerald.routing.handlers import delete, get, post


class UserAPIView(APIView):
    path = "/users"
    permissions = [IsAuthenticated]

    @get(path="/")
    async def all_users(self, request: Request) -> JSONResponse:
        # logic to get all users here
        users = ...

        return JSONResponse({"users": users})

    @get(path="/deny", permissions=[DenyAll], description="API description")
    async def all_usersa(self, request: Request) -> JSONResponse: ...

    @get(path="/allow")
    async def all_usersb(self, request: Request) -> JSONResponse:
        users = ...
        return JSONResponse({"Total Users": users.count()})

    @post(path="/create")
    async def create_user(self, request: Request) -> None:
        # logic to create a user goes here
        ...

    @delete(path="/delete/{user_id}")
    async def delete_user(self, request: Request, user_id: str) -> None:
        # logic to delete a user goes here
        ...

The APIView uses the Esmerald handlers to create the "view" itself but also acts as the parent of those same routes and therefore all the available parameters such as permissions, middlewares, exception handlers, dependencies and almost every other parameter available in the handlers are also available in the APIView.

Parameters

All the parameters and defaults are available in the View Reference.

APIView routing

The routing is the same as declaring the routing for the handler with a simple particularity that you don't need to declare handler by handler. Since everything is inside an APIView objects the handlers will be automatically routed by Esmerald with the joint path given to class.

controllers.py
from esmerald.permissions import DenyAll, IsAuthenticated
from esmerald.requests import Request
from esmerald.responses import JSONResponse
from esmerald.routing.apis.views import APIView
from esmerald.routing.handlers import delete, get, post


class UserAPIView(APIView):
    path = "/users"
    permissions = [IsAuthenticated]

    @get(path="/")
    async def all_users(self, request: Request) -> JSONResponse:
        # logic to get all users here
        users = ...

        return JSONResponse({"users": users})

    @get(path="/deny", permissions=[DenyAll], description="API description")
    async def all_usersa(self, request: Request) -> JSONResponse: ...

    @get(path="/allow")
    async def all_usersb(self, request: Request) -> JSONResponse:
        users = ...
        return JSONResponse({"Total Users": users.count()})

    @post(path="/create")
    async def create_user(self, request: Request) -> None:
        # logic to create a user goes here
        ...

    @delete(path="/delete/{user_id}")
    async def delete_user(self, request: Request, user_id: str) -> None:
        # logic to delete a user goes here
        ...
app.py
from esmerald import Esmerald, Gateway

from .views import UserAPIView

app = Esmerald(routes=[Gateway(handler=UserAPIView)])

APIView path

In the APIView the path is a mandatory field, even if you pass only /. This helps maintaining the structure of the routing cleaner and healthy.

Warning

Just because the APIView is a class it still follows the same rules of the routing priority as well.

Path parameters

APIView is no different from the handlers, really. The same rules for the routing are applied for any route path param.

app.py
from esmerald import APIView, Esmerald, Gateway, get


class MyAPIView(APIView):
    path = "/customer/{name}"

    @get(path="/")
    def home(self, name: str) -> str:  # type: ignore[valid-type]
        return name

    @get(path="/info")
    def info(self, name: str) -> str:  # type: ignore[valid-type]
        return f"Test {name}"

    @get(path="/info/{param}")
    def info_detail(self, name: str, param: str) -> str:  # type: ignore[valid-type]
        return f"Test {name}"


app = Esmerald(routes=[Gateway(handler=MyAPIView)])

Websockets and handlers

The APIView also allows the mix of both HTTP handlers and WebSocket handlers

app.py
from pydantic import BaseModel

from esmerald import APIView, Esmerald, WebSocket, get, websocket
from esmerald.routing.gateways import Gateway


class Item(BaseModel):
    name: str
    sku: str


class MyAPIView(APIView):
    path = "/"

    @get(path="/")
    def get_person(self) -> Item: ...

    @websocket(path="/socket")
    async def ws(self, socket: WebSocket) -> None:
        await socket.accept()
        await socket.send_json({"data": "123"})
        await socket.close()


app = Esmerald(routes=[Gateway(handler=MyAPIView)])

Constraints

When declaring an APIView and registering the route, both Gateway and WebSocketGateway allow to be used for this purpose but one has a limitation compared to the other.

  • Gateway - Allows the APIView to have all the available handlers (get, put, post...) including websocket.
  • WebSocketGateway - Allows only to have websockets.

Generics

Esmerald also offers some generics when it comes to build APIs. For example, the APIView allows the creation of apis where the function name can be whatever you desire like create_users, get_items, update_profile, etc...

Generics in Esmerald are more restrict.

So what does that mean? Means you can only perform operations where the function name coincides with the http verb. For example, get, put, post etc...

If you attempt to create a function where the name differs from a http verb, an ImproperlyConfigured exception is raised unless the extra_allowed is declared.

The available http verbs are:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD
  • OPTIONS
  • TRACE

Basically the same availability as the handlers.

Important

The generics enforce the name matching of the functions with the handlers. That means, if you use a ReadAPIView that only allows the get and you use the wrong handlers on the top of it, for example a post, an ImproperlyConfigured exception will be raised.

Let us see what this means.

from esmerald import post
from esmerald.routing.apis.generics import CreateAPIView


class UserAPI(CreateAPIView):
    """
    ImproperlyConfigured will be raised as the handler `post()`
    name does not match the function name `post`.
    """

    @post()
    async def get(self) -> str: ...

As you can see, the handler post() does not match the function name get. It should always match.

An easy way of knowing this is simple, when it comes to the available http verbs, the function name should always match the handler.

Are there any exception? Yes but not for these specific cases, the exceptions are called extra_allowed but more details about this later on.

SimpleAPIView

This is the base of all generics, subclassing from this class will allow you to perform all the available http verbs without any restriction.

This is how you can import.

from esmerald import SimpleAPIView

Example

from esmerald import SimpleAPIView, delete, get, patch, post, put


class UserAPI(SimpleAPIView):
    @get()
    async def get(self) -> str: ...

    @post()
    async def post(self) -> str: ...

    @put()
    async def put(self) -> str: ...

    @patch()
    async def patch(self) -> str: ...

    @delete()
    async def delete(self) -> None: ...

ReadAPIView

Allows the GET verb to be used.

This is how you can import.

from esmerald.routing.apis.generics import ReadAPIView

Example

from esmerald import get
from esmerald.routing.apis.generics import ReadAPIView


class UserAPI(ReadAPIView):
    """
    ReadAPIView only allows the `get` to be used by default.
    """

    @get()
    async def get(self) -> str: ...

CreateAPIView

Allows the POST, PUT, PATCH verbs to be used.

This is how you can import.

from esmerald.routing.apis.generics import CreateAPIView

Example

from esmerald import patch, post, put
from esmerald.routing.apis.generics import CreateAPIView


class UserAPI(CreateAPIView):
    """
    CreateAPIView only allows the `post`, `put` and `patch`
    to be used by default.
    """

    @post()
    async def post(self) -> str: ...

    @put()
    async def put(self) -> str: ...

    @patch()
    async def patch(self) -> str: ...

DeleteAPIView

Allows the DELETE verb to be used.

This is how you can import.

from esmerald.routing.apis.generics import DeleteAPIView

Example

from esmerald import delete
from esmerald.routing.apis.generics import DeleteAPIView


class UserAPI(DeleteAPIView):
    """
    DeleteAPIView only allows the `delete` to be used by default.
    """

    @delete()
    async def delete(self) -> None: ...

Combining all in one

What if you want to combine them all? Of course you also can.

from esmerald import delete, get, patch, post, put
from esmerald.routing.apis.generics import CreateAPIView, DeleteAPIView, ReadAPIView


class UserAPI(CreateAPIView, DeleteAPIView, ReadAPIView):
    """
    Combining them all.
    """

    @get()
    async def get(self) -> str: ...

    @post()
    async def post(self) -> str: ...

    @put()
    async def put(self) -> str: ...

    @patch()
    async def patch(self) -> str: ...

    @delete()
    async def delete(self) -> None: ...

Combining them all is the same as using the SimpleAPIView.

ListAPIView

This is a nice to have type of generic. In principle, all the functions must return lists or None of any kind.

This generic enforces the return annotations to always be lists or None.

Allows all the verbs be used.

This is how you can import.

from esmerald.routing.apis.generics import ListAPIView

Example

from typing import List

from esmerald import get, patch, post, put
from esmerald.routing.apis.generics import ListAPIView


class UserAPI(ListAPIView):
    @get()
    async def get(self) -> List[str]: ...

    @post()
    async def post(self) -> List[str]: ...

    @put()
    async def put(self) -> List[str]: ...

    @patch()
    async def patch(self) -> List[str]: ...

This is another generic that follows the same rules of the SimpleAPIView, which means, if you want to add extra functions such as a read_item() or anything else, you must follow the extra allowed principle.

from typing import List

from esmerald import get, patch, post, put
from esmerald.routing.apis.generics import ListAPIView


class UserAPI(ListAPIView):
    extra_allowed: List[str] = ["read_item"]

    @post()
    async def post(self) -> List[str]: ...

    @put()
    async def put(self) -> List[str]: ...

    @patch()
    async def patch(self) -> List[str]: ...

    @get()
    async def read_item(self) -> List[str]: ...

extra_allowed

All the generics subclass the SimpleAPIView as mentioned before and that superclass uses the http_allowed_methods to verify which methods are allowed or not to be passed inside the API object but also check if there is any extra_allowed list with any extra functions you would like the view to deliver.

This means that if you want to add a read_item() function to any of the generics you also do it easily.

from typing import List

from esmerald import get, patch, post, put
from esmerald.routing.apis.generics import CreateAPIView


class UserAPI(CreateAPIView):
    """
    CreateAPIView only allows the `post`, `put` and `patch`
    to be used by default.
    """

    extra_allowed: List[str] = ["read_item"]

    @post()
    async def post(self) -> str: ...

    @put()
    async def put(self) -> str: ...

    @patch()
    async def patch(self) -> str: ...

    @get()
    async def read_item(self) -> str: ...

As you can see, to make it happen you would need to declare the function name inside the extra_allowed to make sure that an ImproperlyConfigured is not raised.

What to choose

All the available objects from the APIView to the SimpleAPIView and generics can do whatever you want and need so what and how to choose the right one for you?

Well, like everything, it will depend of what you want to achieve. For example, if you do not care or do not want to be bothered with http_allowed_methods and want to go without restrictions, then the APIView is the right choice for you.

On the other hand, if you feel like restricting youself or even during development you might want to restrict some actions on the fly, so maybe you can opt for choosing the SimpleAPIView or any of the generics.

Your take!