Skip to content

OpenAPI

Esmerald as mentioned across the documentation supports natively the automatic generation of the API docs in three different ways:

  • Swagger - Defaults to /docs/swagger.
  • Redoc - Defaults to /docs/redoc.
  • Stoplight - Defaults to /docs/elements.

Tip

See the OpenAPIConfig for more details how to take advantage of the defaults provided and how to change them.

The OpenAPIConfig

The OpenAPIConfig configuration explains in more detail what and how to use it.

How to use it

There are many things you can do with the OpenAPI, from simple calls to authentication using the docs.

Let us assume we have some apis for a user that will handle simple CRUD that belongs to a blog project.

Note

We will not be dwelling on the technicalities of the database models but for this example it was used the Edgy contrib from Esmerald as it speeds up the development.

The APIs look like this:

from typing import List

from esmerald import Request, delete, get, post, put
from esmerald.openapi.datastructures import OpenAPIResponse

from .daos import UserDAO
from .schemas import Error, UserIn, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()


@get(
    "/{id}",
    tags=["User"],
    summary="Get a user",
    description="Shows the information of a user",
    responses={
        200: OpenAPIResponse(model=UserOut),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
)
async def user(id: int) -> UserOut:
    """
    Get the information about a user
    """
    user = UserDAO()
    return await user.get(obj_id=id)


@post(
    "/create",
    tags=["User"],
    summary="Create a user",
    description="Creates a user in the system",
    responses={400: OpenAPIResponse(model=Error, description="Bad response")},
)
async def create(data: UserIn) -> None:
    """
    Creates a user in the system.
    """
    user = UserDAO()
    await user.create(**data.model_dump())


@put(
    "/{id}",
    tags=["User"],
    summary="Updates a user",
    description="Updates a user in the system",
    responses={400: OpenAPIResponse(model=Error, description="Bad response")},
)
async def update(data: UserIn, id: int) -> None:
    """
    Updates a user in the system.
    """
    user = UserDAO()
    await user.update(id, **data.model_dump())


@delete(
    "/{id}",
    summary="Delete a user",
    tags=["User"],
    description="Deletes a user from the system by ID",
    responses={
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
)
async def delete_user(id: int) -> None:
    """
    Deletes a user.
    """
    user = UserDAO()
    await user.delete(obj_id=id)

The daos and schemas are simply placed in different files but you get the gist of it.

Tip

If you are not familiar with the DAO, have a look at the official explanation and how you can also use it.

Now it is time to see how it would look like using the official documentation.

Swagger

Accessing the default /docs/swagger, you should be able to see something like this:

And expanding one of the APIs:

Including the normal responses:

Redoc

What if you prefer redoc instead? Well, you can simply access the /docs/redoc and you should be able to see something like this:

Stoplight

Esmerald also offers the Stoplight elements documentation. Accessing /docs/elements you should be able to see something like this:

Authentication in documentation

Now this is where the things get interesting. There are cases where the majority of your APIs will be behind some sort of authentication and permission system and to access the data of those APIs and test them directly in your docs is a must.

Esmerald comes with a pre-defined set of utilities that you can simply add you your APIs and enable the authentication via documentation.

The security attribute is what Esmerald looks for when generating the docs for you and there is where you can pass the definitions needed.

Supported authorizations

  • Basic - For basic authentication.
  • Bearer - For the Authorization of a Bearer token. Example: JWT token authentication.
  • Digest - For digest.
  • APIKeyInCookie - For any key passed in a cookie with a spefific name.
  • APIKeyInHeader - For any key passed in a header with a spefific name.
  • APIKeyInQuery - For any key passed in a query with a spefific name.
  • OAuth2 - For OAuth2 authentication.
  • OpenIdConnect - OpenIdConnect authorization.

How to import them:

from esmerald.openapi.security.api_key import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery
from esmerald.openapi.security.http import Basic, Bearer, Digest
from esmerald.openapi.security.oauth2 import OAuth2
from esmerald.openapi.security.openid_connect import OpenIdConnect

HTTPBase

Every supported authorization has the same HTTPBase which means if you want to build your own custom object, you can simply inherit from it and develop it.

from esmerald.openapi.security.base import HTTPBase

Parameters

Every supported authorization has in common the following parameters:

  • type_ - The type of security scheme. Literal apiKey, http, mutualTLS, oauth2 or openIdConnect.
  • scheme_name (Optional) - The name for the scheme to be shown in the docs.

    Default: __class__.__name__

  • description (Optional) - A description for the security scheme.

  • in_ (Optional) - The location of the API key. Literal header, cookie or query.
  • name (Optional) - The name of the header, query or cookie parameter to be used. This is should be used when using APIKeyInCookie, APIKeyInHeader or APIKeyInQuery.
  • scheme - The name of the HTTP Authorization scheme to be used in the Authorization header as defined in RFC7235. Example: Authorization.

How to use it

Now that we are more acquainted with the supported authorization, let us see how you could use them.

Let us use the following API as example from before.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

Basic

Without instantiating the object.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.http import Basic

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[Basic],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

As an instance in case you need to pass extra parameters.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.http import Basic

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[Basic()],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

Bearer

Without instantiating the object.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.http import Bearer

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[Bearer],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

As an instance in case you need to pass extra parameters.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.http import Bearer

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[Bearer()],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

Digest

Without instantiating the object.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.http import Digest

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[Digest],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

As an instance in case you need to pass extra parameters.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.http import Digest

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[Digest()],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

APIKeyInHeader

Without instantiating the object.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.api_key import APIKeyInHeader

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[APIKeyInHeader],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

This example does not do too much since you are not specifying the name of the header to be passed.

As an instance in case you need to pass extra parameters.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.api_key import APIKeyInHeader

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[APIKeyInHeader(name="X_TOKEN_API")],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

This now should be the way of declaring it there the name is X_TOKEN_API and this will automatically added in your API calls that declare it.

APIKeyInCookie

Without instantiating the object.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.api_key import APIKeyInCookie

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[APIKeyInCookie],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

This example does not do too much since you are not specifying the name of the cookie to be passed.

As an instance in case you need to pass extra parameters.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.api_key import APIKeyInCookie

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[APIKeyInCookie(name="X_COOKIE_API")],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

This now should be the way of declaring it there the name is X_COOKIE_API and this will automatically added in your API calls that declare it.

APIKeyInQuery

Without instantiating the object.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.api_key import APIKeyInQuery

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[APIKeyInQuery],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

This example does not do too much since you are not specifying the name of the query parameter to be passed.

As an instance in case you need to pass extra parameters.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.api_key import APIKeyInQuery

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[APIKeyInQuery(name="X_QUERY_API")],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

This now should be the way of declaring it there the name is X_QUERY_API and this will automatically added in your API calls that declare it by adding the ?X_QUERY_API=<VALUE>.

OAuth2

Now this one is quite tricky because this one also expects flows to be passed for the oauth2 to take place.

The flows is expecting an object of type OAuthFlows.

OAuthFlows
from esmerald.openapi.models import OAuthFlows
OAuthFlow

Each parameter of the OAuthFlows is an instance of an OAuthFlow.

from esmerald.openapi.models import OAuthFlow

Parameters:

  • authorizationUrl - For oauth2 ("implicit", "authorizationCode"). The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
  • tokenUrl - For oauth2 ("password", "clientCredentials", "authorizationCode"). The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.
  • scopes - The available scopes for the OAuth2 security scheme. A map between the scope name and a short description for it. The map can be empty.
  • refreshUrl (Optional) - The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 standard requires the use of TLS.

Example:

from esmerald.openapi.models import OAuthFlows, OAuthFlow


implicit: OAuthFlow = OAuthFlow(...)
password: OAuthFlow = OAuthFlow(...)
clientCredentials: OAuthFlow = OAuthFlow(...)
authorizationCode: OAuthFlow = OAuthFlow(...)

OAuthFlows(
    implicit=implicit,
    password=password,
    clientCredentials=clientCredentials,
    authorizationCode=authorizationCode,
)
Using it in APIs

We won't pass all the details here but you now understand that for OAuth2 certain parameters are needed.

As an instance in case you need to pass extra parameters.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.oauth2 import OAuth2

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[OAuth2(...)],  # With all the required parameters
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

OpenIdConnect

The openIdConnect requires you to specify a openIdConnectUrl parameter.

  • openIdConnectUrl - OpenId Connect URL to discover OAuth2 configuration values. This MUST be in the form of a URL. The OpenID Connect standard requires the use of TLS.

As an instance in case you need to pass extra parameters.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.openid_connect import OpenIdConnect

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[OpenIdConnect(openIdConnectUrl=...)],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

Combine them all

Is it possible to have more than one type in the APIs? Of course!.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.api_key import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery
from esmerald.openapi.security.http import Basic, Bearer, Digest
from esmerald.openapi.security.oauth2 import OAuth2
from esmerald.openapi.security.openid_connect import OpenIdConnect

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[
        Basic,
        Bearer,
        Digest,
        APIKeyInHeader(name="X_TOKEN_API"),
        APIKeyInCookie(name="X_QUERY_API"),
        APIKeyInQuery(name="X_COOKIE_API"),
        OpenIdConnect(openIdConnectUrl=...),
    ],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

Check the documentation

With all the authentication methods added to your APIs you can now check the docs for something like this:

The Autorize will show and you can simply use whatever authentication method you decided to have.

Let us see how it would look like if we have APIKeyInHeader, APIKeyInCookie and APIKeyInQuery.

from typing import List

from esmerald import Request, get
from esmerald.openapi.datastructures import OpenAPIResponse
from esmerald.openapi.security.api_key import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery

from .daos import UserDAO
from .schemas import Error, UserOut


@get(
    "/users",
    tags=["User"],
    description="List of all the users in the system",
    summary="Lists all users",
    responses={
        200: OpenAPIResponse(model=[UserOut]),
        400: OpenAPIResponse(model=Error, description="Bad response"),
    },
    security=[
        APIKeyInHeader(name="X_TOKEN_API"),
        APIKeyInCookie(name="X_COOKIE_API"),
        APIKeyInQuery(name="X_QUERY_API"),
    ],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

You should see something like this when Authorize is called.

Did you notice the name specified in each authorization object? Cool, right?.

Levels

Like everything in Esmerald, you can specify the security on each level of the application. Which means, you don't need to repeat yourself if for instance, all APIs of a given Include require a Bearer token or any other.