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 theAuthorization
of aHTTPBearer
token. Example:JWT
token authentication.HTTPDigest
- For digest.APIKeyInCookie
- For any key passed in acookie
with a spefificname
.APIKeyInHeader
- For any key passed in aheader
with a spefificname
.APIKeyInQuery
- For any key passed in aquery
with a spefificname
.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
oropenIdConnect
. -
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
orAPIKeyInQuery
. - 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.