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.
  • Rapidoc - /docs/rapidoc.

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

  • HTTPBasic - For basic authentication.
  • HTTPBearer - For the Authorization of a HTTPBearer token. Example: JWT token authentication.
  • HTTPDigest - 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. dasda How to import them:
from esmerald.security.api_key import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery
from esmerald.security.http import HTTPBasic, HTTPBearer, HTTPDigest
from esmerald.security.oauth2 import OAuth2
from esmerald.security.open_id 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.security 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__

  • scheme - The name of the HTTP Authorization scheme to be used in the Authorization header as defined in RFC7235. Example: Authorization.

  • scheme_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.
  • description (Optional) - A description for the security scheme.

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.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()

HTTPBasic

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.security.http import HTTPBasic

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=[HTTPBasic()],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

HTTPBearer

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.security.http import HTTPBearer

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=[HTTPBearer()],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

HTTPDigest

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.security.http import HTTPDigest

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=[HTTPDigest()],
)
async def users(request: Request) -> List[UserOut]:
    """
    Lists all the users in the system.
    """
    users = UserDAO()
    return await users.get_all()

APIKeyInHeader

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.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

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.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

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.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 is an extremely complex and dedicated flow. Esmerald provides detailed explanations and examples in its own security section, including how to use it in the OpenAPI documentation.

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.security.open_id 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="/openid")],
)
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.security.api_key import APIKeyInCookie, APIKeyInHeader, APIKeyInQuery
from esmerald.security.http import HTTPBearer, HTTPBasic, HTTPDigest
from esmerald.security.oauth2 import OAuth2
from esmerald.security.open_id import OpenIdConnect
from esmerald.security.http import HTTPBasic

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=[
        HTTPBasic(),
        HTTPBearer(),
        HTTPDigest(),
        APIKeyInHeader(name="X_TOKEN_API"),
        APIKeyInCookie(name="X_QUERY_API"),
        APIKeyInQuery(name="X_COOKIE_API"),
        OpenIdConnect(openIdConnectUrl="/openid"),
        OAuth2(
            flows={
                "password": {
                    "tokenUrl": "token",
                    "scopes": {"read:users": "Read the users", "write:users": "Create users"},
                }
            },
            description="OAuth2 security scheme",
        ),
    ],
)
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.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 HTTPBearer token or any other.