Перейти к содержанию

Responses

Warning

The current page still doesn't have a translation for this language.

But you can help translating it: Contributing.

Like every application, there are many different responses that can be used for different use cases and scenarios.

Esmerald having Lilya under the hood also means that all available responses from it simply just work.

You simply just need to decide which type of response your function will have and let Esmerald take care of the rest.

Tip

Esmerald automatically understands if you are typing/returning a dataclass, a Pydantic dataclass or a Pydantic model and converts them automatically into a JSON response.

Esmerald responses and the application

The available responses from Esmerald are:

  • Response
  • JSON
  • ORJSON
  • UJSON
  • Template
  • Redirect
  • File
  • Stream

Important requirements

Some responses use extra dependencies, such as UJSON and ORJSON. To use these responses, you need to install:

$ pip install ujson orjson

This will allow you to use the ORJSON and UJSON as well as the UJSONResponse and ORJSONResponse in your projects.

Response

Classic and generic Response that fits almost every single use case out there. It behaves like lilya Response by default. It has a special mode for media_type="application/json" (used by handlers) in which it behaves like make_response (encodes content). This special mode is required for encoding data like datetimes, ...

from esmerald import (
    APIView,
    Esmerald,
    Gateway,
    Request,
    Response,
    WebSocket,
    get,
    post,
    status,
    websocket,
)


class World(APIView):
    @get(path="/{url}")
    async def home(self, request: Request, url: str) -> Response:
        return Response(f"URL: {url}")

    @post(path="/{url}", status_code=status.HTTP_201_CREATED)
    async def mars(self, request: Request, url: str) -> Response: ...

    @websocket(path="/{path_param:str}")
    async def pluto(self, socket: WebSocket) -> None:
        await socket.accept()
        msg = await socket.receive_json()
        assert msg
        assert socket
        await socket.close()


app = Esmerald(routes=[Gateway("/world", handler=World)])

Or a unique custom response:

from pydantic import BaseModel

from esmerald import Esmerald, Gateway, Response, get
from esmerald.datastructures import Cookie


class Item(BaseModel):
    id: int
    sku: str


@get(path="/me")
async def home() -> Response[Item]:
    return Response(
        Item(id=1, sku="sku1238"),
        headers={"SKY-HEADER": "sku-xyz"},
        cookies=[Cookie(key="sku", value="a-value")],
    )


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

Esmerald supports good design, structure and practices but does not force you to follow specific rules of anything unless you want to.

API Reference

Check out the API Reference for Response for more details.

JSON

The classic JSON response for 99% of the responses used nowaday. The JSON returns a JSONResponse (ORJSON).

from esmerald import JSON, APIView, Esmerald, Gateway, Request, get, post, status


class World(APIView):
    @get(path="/{url}")
    async def home(self, request: Request, url: str) -> JSON:
        return JSON(content=f"URL: {url}")

    @post(path="/{url}", status_code=status.HTTP_201_CREATED)
    async def mars(self, request: Request, url: str) -> JSON: ...


app = Esmerald(routes=[Gateway("/world", handler=World)])

API Reference

Check out the API Reference for JSON for more details.

JSONResponse (Lilya)

You can always use directly the JSONResponse from Lilya without using the Esmerald wrapper.

from lilya.responses import JSONResponse as JSONResponse

API Reference

Check out the API Reference for JSONResponse for more details.

ORJSON

Super fast JSON serialization/deserialization response.

from esmerald import APIView, Esmerald, Gateway, Request, get, post, status
from esmerald.datastructures.encoders import OrJSON


class World(APIView):
    @get(path="/{url}")
    async def home(self, request: Request, url: str) -> OrJSON:
        return OrJSON(content=f"URL: {url}")

    @post(path="/{url}", status_code=status.HTTP_201_CREATED)
    async def mars(self, request: Request, url: str) -> OrJSON: ...


app = Esmerald(routes=[Gateway("/world", handler=World)])

Warning

Please read the important requirements before using this response.

Check

More details about the ORJSON can be found here.

API Reference

Check out the API Reference for OrJSON for more details.

ORJSONResponse

You can always use directly the ORJSONResponse from Esmerald without using the wrapper.

from esmerald.responses.encoders import ORJSONResponse

or alternatively (we alias JSONResponse to ORJSONResponse because it is faster)

from esmerald.responses import JSONResponse

API Reference

Check out the API Reference for ORJSONResponse for more details.

UJSON

Another super fast JSON serialization/deserialization response.

from esmerald import APIView, Esmerald, Gateway, Request, get, post, status
from esmerald.datastructures.encoders import UJSON


class World(APIView):
    @get(path="/{url}")
    async def home(self, request: Request, url: str) -> UJSON:
        return UJSON(content=f"URL: {url}")

    @post(path="/{url}", status_code=status.HTTP_201_CREATED)
    async def mars(self, request: Request, url: str) -> UJSON: ...


app = Esmerald(routes=[Gateway("/world", handler=World)])

Warning

Please read the important requirements before using this response.

Check

More details about the UJSON can be found here. For JSONResponse the way of doing it the same as ORJSONResponse and UJSONResponse.

API Reference

Check out the API Reference for UJSON for more details.

UJSONResponse

You can always use directly the UJSONResponse from Esmerald without using the wrapper.

from esmerald.responses.encoders import UJSONResponse

API Reference

Check out the API Reference for UJSONResponse for more details.

Template

As the name suggests, it is the response used to render HTML templates.

This response returns a TemplateResponse.

from esmerald import Esmerald, Gateway, Template, get
from esmerald.datastructures import Cookie, ResponseHeader


@get(
    path="/home",
    response_headers={"local-header": ResponseHeader(value="my-header")},
    response_cookies=[
        Cookie(key="redirect-cookie", value="redirect-cookie"),
        Cookie(key="general-cookie", value="general-cookie"),
    ],
)
def home() -> Template:
    return Template(
        name="my-tem",
        context={"user": "me"},
        alternative_template=...,
    )


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

API Reference

Check out the API Reference for Template for more details.

Redirect

As the name indicates, it is the response used to redirect to another endpoint/path.

This response returns a ResponseRedirect.

from esmerald import Esmerald, Gateway, Redirect, get


@get("/another-home")
async def another_home() -> str:
    return "another-home"


@get(
    path="/home",
)
def home() -> Redirect:
    return Redirect(path="/another-home")


app = Esmerald(routes=[Gateway(handler=home), Gateway(handler=another_home)])

API Reference

Check out the API Reference for Redirect for more details.

File

The File response sends a file. This response returns a FileResponse.

from esmerald import Esmerald, Gateway, get
from esmerald.datastructures import File


@get(
    path="/download",
)
def download() -> File:
    return File(
        path="/path/to/file",
        filename="download.csv",
    )


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

API Reference

Check out the API Reference for File for more details.

Stream

The Stream response uses the StreamResponse.

from typing import Generator

from esmerald import Esmerald, Gateway, get
from esmerald.datastructures import Stream


def my_generator() -> Generator[str, None, None]:
    count = 0
    while True:
        count += 1
        yield str(count)


@get(
    path="/stream",
)
def stream() -> Stream:
    return Stream(
        iterator=my_generator(),
    )


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

API Reference

Check out the API Reference for Stream for more details.

Important notes

Template, Redirect, File and Stream are wrappers around the Lilya TemplateResponse, RedirectResponse, FileResponse and StreamResponse.

Those responses are also possible to be used directly without the need of using the wrapper.

The wrappers, like Lilya, also accept the classic parameters such as headers and cookies.

Response status codes

You need to be mindful when it comes to return a specific status code when using JSON, ORJSON and UJSON wrappers.

Esmerald allows you to pass the status codes via handler and directly via return of that same response but the if the handler has a status_code declared, the returned status_code takes precedence.

Let us use an example to be more clear. This example is applied to JSON, UJSON and ORJSON.

Status code without declaring in the handler

from esmerald import JSON, Esmerald, Gateway, Request, get, status


@get(path="/{url}")
async def home(request: Request, url: str) -> JSON:
    return JSON(content=f"URL: {url}", status_code=status.HTTP_202_ACCEPTED)


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

In this example, the returned status code will be 202 Accepted as it was declared directly in the response and not in the handler.

Status code declared in the handler

from esmerald import JSON, Esmerald, Gateway, Request, get, status


@get(path="/{url}", status_code=status.HTTP_202_ACCEPTED)
async def home(request: Request, url: str) -> JSON:
    return JSON(content=f"URL: {url}")


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

In this example, the returned status code will also be 202 Accepted as it was declared directly in the handler response and not in the handler.

Status code declared in the handler and in the return

Now what happens if we declare the status_code in both?

from esmerald import JSON, Esmerald, Gateway, Request, get, status


@get(path="/{url}", status_code=status.HTTP_201_CREATED)
async def home(request: Request, url: str) -> JSON:
    return JSON(content=f"URL: {url}", status_code=status.HTTP_202_ACCEPTED)


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

This will return 202 Accepted and not 201 Created and the reason for that is because the return takes precedence over the handler.

OpenAPI Responses

This is a special attribute that is used for OpenAPI specification purposes and can be created and added to a specific handler. You can add one or multiple different responses into your specification.

from typing import Union

from esmerald import post
from esmerald.openapi.datastructures import OpenAPIResponse
from pydantic import BaseModel


class ItemOut(BaseModel):
    sku: str
    description: str


@post(path='/create', summary="Creates an item", responses={200: OpenAPIResponse(model=ItemOut, description=...)})
async def create() -> Union[None, ItemOut]:
    ...

This will add an extra response description and details to your OpenAPI spec handler definition.

API Reference

Check out the API Reference for OpenAPIResponse for more details.

Important

When adding an OpenAPIResponse you can also vary and override the defaults of each handler. For example, the @post defaults to 201 but you might want to add a different response.

from typing import Union

from esmerald import post
from esmerald.openapi.datastructures import OpenAPIResponse
from pydantic import BaseModel


class ItemOut(BaseModel):
    sku: str
    description: str


@post(path='/create', summary="Creates an item", responses={201: OpenAPIResponse(model=ItemOut, description=...)})
async def create() -> Union[None, ItemOut]:
    ...

You also might want to add more than just one response to the handler, for instance, a 401 or any other.

from typing import Union

from esmerald import post
from esmerald.openapi.datastructures import OpenAPIResponse
from pydantic import BaseModel


class ItemOut(BaseModel):
    sku: str
    description: str


class Error(BaseModel):
    detail: str
    line_number: int


@post(path='/create', summary="Creates an item", responses={
        201: OpenAPIResponse(model=ItemOut, description=...),
        401: OpenAPIResponse(model=Error, description=...),
    }
)
async def create() -> Union[None, ItemOut]:
    ...

Lists

What if you want to specify in the response that you would like to have a list (array) of returned objects?

Let us imagine we want to return a list of an item in one endpoint and a list of users in another.

from typing import Union

from esmerald import post
from esmerald.openapi.datastructures import OpenAPIResponse
from pydantic import BaseModel, EmailStr


class ItemOut(BaseModel):
    sku: str
    description: str


class UserOut(BaseModel):
    name: str
    email: EmailStr


@get(path='/items', summary="Get all the items", responses={
        201: OpenAPIResponse(model=[ItemOut], description=...),
    }
)
async def get_items() -> Union[None, ItemOut]:
    ...

@get(path='/users', summary="Get all the users", responses={
        201: OpenAPIResponse(model=[UserOut], description=...),
    }
)
async def get_items() -> Union[None, UserOut]:
    ...

As you could notice, we simply added [] in the model to reflect a list in the OpenAPI specification. That simple.

Errors

A ValueError is raised in the following scenarios:

  • You try to pass a model than one pydantic model into a list. The OpenAPIResponse is a mere representation of a response, so be compliant.
  • You try to pass a model that is not a subclass of a Pydantic BaseModel.
  • You try to pass a list of non Pydantic BaseModels.

When one of these scenarios occur, the following error will be raised.

The representation of a list of models in OpenAPI can only be a total of one. Example: OpenAPIResponse(model=[MyModel])

Other responses

There are other responses you can have that does not necessessarily have to be the ones provided here. Every case is unique and you might want to return directly a string, a dict, an integer, a list or whatever you want.

from pydantic import EmailStr

from esmerald import Esmerald, Gateway, Request, get, post


@get(path="/me")
async def home(request: Request) -> EmailStr:
    return request.user.email


@post(path="/create")
async def create(request: Request) -> str:
    return "OK"


app = Esmerald(routes=[Gateway(handler=home), Gateway(handler=create)])

Example

Below we have a few examples of possible responses recognised by Esmerald automatically.

Pydantic model

import httpx
from pydantic import BaseModel

from esmerald import Esmerald, Form, Gateway, post


class User(BaseModel):
    name: str
    email: str


@post("/create")
async def create(data: User = Form()) -> User:
    """
    Creates a user in the system and does not return anything.
    Default status_code: 201
    """
    return data


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

# Payload example
data = {"name": "example", "email": "example@esmerald.dev"}

# Send the request
httpx.post("/create", data=data)

Pydantic dataclass

import httpx
from pydantic.dataclasses import dataclass

from esmerald import Esmerald, Form, Gateway, post


@dataclass
class User:
    name: str
    email: str


@post("/create")
async def create(data: User = Form()) -> User:
    """
    Creates a user in the system and does not return anything.
    Default status_code: 201
    """
    return data


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

# Payload example
data = {"name": "example", "email": "example@esmerald.dev"}

# Send the request
httpx.post("/create", data=data)

Python dataclass

from dataclasses import dataclass

import httpx

from esmerald import Esmerald, Form, Gateway, post


@dataclass
class User:
    name: str
    email: str


@post("/create")
async def create(data: User = Form()) -> User:
    """
    Creates a user in the system and does not return anything.
    Default status_code: 201
    """
    return data


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

# Payload example
data = {"name": "example", "email": "example@esmerald.dev"}

# Send the request
httpx.post("/create", data=data)

MsgSpec

from typing import Union

from esmerald import Esmerald, Gateway, post
from esmerald.datastructures.msgspec import Struct


class User(Struct):
    name: str
    email: Union[str, None] = None


@post()
def create(data: User) -> User:
    """
    Returns the same payload sent to the API.
    """
    return data


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