OpenAPI Callbacks¶
Podrías crear una API con una path operation que podría desencadenar un request a una API externa creada por alguien más (probablemente el mismo desarrollador que estaría usando tu API).
El proceso que ocurre cuando tu aplicación API llama a la API externa se llama un "callback". Porque el software que escribió el desarrollador externo envía un request a tu API y luego tu API responde, enviando un request a una API externa (que probablemente fue creada por el mismo desarrollador).
En este caso, podrías querer documentar cómo esa API externa debería verse. Qué path operation debería tener, qué cuerpo debería esperar, qué response debería devolver, etc.
Una aplicación con callbacks¶
Veamos todo esto con un ejemplo.
Imagina que desarrollas una aplicación que permite crear facturas.
Estas facturas tendrán un id
, title
(opcional), customer
, y total
.
El usuario de tu API (un desarrollador externo) creará una factura en tu API con un request POST.
Luego tu API (imaginemos):
- Enviará la factura a algún cliente del desarrollador externo.
- Recogerá el dinero.
- Enviará una notificación de vuelta al usuario de la API (el desarrollador externo).
- Esto se hará enviando un request POST (desde tu API) a alguna API externa proporcionada por ese desarrollador externo (este es el "callback").
La aplicación normal de FastAPI¶
Primero veamos cómo sería la aplicación API normal antes de agregar el callback.
Tendrá una path operation que recibirá un cuerpo Invoice
, y un parámetro de query callback_url
que contendrá la URL para el callback.
Esta parte es bastante normal, probablemente ya estés familiarizado con la mayor parte del código:
from typing import Union
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Invoice(BaseModel):
id: str
title: Union[str, None] = None
customer: str
total: float
class InvoiceEvent(BaseModel):
description: str
paid: bool
class InvoiceEventReceived(BaseModel):
ok: bool
invoices_callback_router = APIRouter()
@invoices_callback_router.post(
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived
)
def invoice_notification(body: InvoiceEvent):
pass
@app.post("/invoices/", callbacks=invoices_callback_router.routes)
def create_invoice(invoice: Invoice, callback_url: Union[HttpUrl, None] = None):
"""
Create an invoice.
This will (let's imagine) let the API user (some external developer) create an
invoice.
And this path operation will:
* Send the invoice to the client.
* Collect the money from the client.
* Send a notification back to the API user (the external developer), as a callback.
* At this point is that the API will somehow send a POST request to the
external API with the notification of the invoice event
(e.g. "payment successful").
"""
# Send the invoice, collect the money, send the notification (the callback)
return {"msg": "Invoice received"}
Consejo
El parámetro de query callback_url
utiliza un tipo Url de Pydantic.
Lo único nuevo es el callbacks=invoices_callback_router.routes
como un argumento para el decorador de path operation. Veremos qué es eso a continuación.
Documentar el callback¶
El código real del callback dependerá mucho de tu propia aplicación API.
Y probablemente variará mucho de una aplicación a otra.
Podría ser solo una o dos líneas de código, como:
callback_url = "https://example.com/api/v1/invoices/events/"
httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})
Pero posiblemente la parte más importante del callback es asegurarse de que el usuario de tu API (el desarrollador externo) implemente la API externa correctamente, de acuerdo con los datos que tu API va a enviar en el request body del callback, etc.
Entonces, lo que haremos a continuación es agregar el código para documentar cómo debería verse esa API externa para recibir el callback de tu API.
Esa documentación aparecerá en la Swagger UI en /docs
en tu API, y permitirá a los desarrolladores externos saber cómo construir la API externa.
Este ejemplo no implementa el callback en sí (eso podría ser solo una línea de código), solo la parte de documentación.
Consejo
El callback real es solo un request HTTP.
Cuando implementes el callback tú mismo, podrías usar algo como HTTPX o Requests.
Escribir el código de documentación del callback¶
Este código no se ejecutará en tu aplicación, solo lo necesitamos para documentar cómo debería verse esa API externa.
Pero, ya sabes cómo crear fácilmente documentación automática para una API con FastAPI.
Así que vamos a usar ese mismo conocimiento para documentar cómo debería verse la API externa... creando la(s) path operation(s) que la API externa debería implementar (las que tu API va a llamar).
Consejo
Cuando escribas el código para documentar un callback, podría ser útil imaginar que eres ese desarrollador externo. Y que actualmente estás implementando la API externa, no tu API.
Adoptar temporalmente este punto de vista (del desarrollador externo) puede ayudarte a sentir que es más obvio dónde poner los parámetros, el modelo de Pydantic para el body, para el response, etc. para esa API externa.
Crear un APIRouter
de callback¶
Primero crea un nuevo APIRouter
que contendrá uno o más callbacks.
from typing import Union
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Invoice(BaseModel):
id: str
title: Union[str, None] = None
customer: str
total: float
class InvoiceEvent(BaseModel):
description: str
paid: bool
class InvoiceEventReceived(BaseModel):
ok: bool
invoices_callback_router = APIRouter()
@invoices_callback_router.post(
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived
)
def invoice_notification(body: InvoiceEvent):
pass
@app.post("/invoices/", callbacks=invoices_callback_router.routes)
def create_invoice(invoice: Invoice, callback_url: Union[HttpUrl, None] = None):
"""
Create an invoice.
This will (let's imagine) let the API user (some external developer) create an
invoice.
And this path operation will:
* Send the invoice to the client.
* Collect the money from the client.
* Send a notification back to the API user (the external developer), as a callback.
* At this point is that the API will somehow send a POST request to the
external API with the notification of the invoice event
(e.g. "payment successful").
"""
# Send the invoice, collect the money, send the notification (the callback)
return {"msg": "Invoice received"}
Crear la path operation del callback¶
Para crear la path operation del callback utiliza el mismo APIRouter
que creaste anteriormente.
Debería verse como una path operation normal de FastAPI:
- Probablemente debería tener una declaración del body que debería recibir, por ejemplo
body: InvoiceEvent
. - Y también podría tener una declaración del response que debería devolver, por ejemplo
response_model=InvoiceEventReceived
.
from typing import Union
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Invoice(BaseModel):
id: str
title: Union[str, None] = None
customer: str
total: float
class InvoiceEvent(BaseModel):
description: str
paid: bool
class InvoiceEventReceived(BaseModel):
ok: bool
invoices_callback_router = APIRouter()
@invoices_callback_router.post(
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived
)
def invoice_notification(body: InvoiceEvent):
pass
@app.post("/invoices/", callbacks=invoices_callback_router.routes)
def create_invoice(invoice: Invoice, callback_url: Union[HttpUrl, None] = None):
"""
Create an invoice.
This will (let's imagine) let the API user (some external developer) create an
invoice.
And this path operation will:
* Send the invoice to the client.
* Collect the money from the client.
* Send a notification back to the API user (the external developer), as a callback.
* At this point is that the API will somehow send a POST request to the
external API with the notification of the invoice event
(e.g. "payment successful").
"""
# Send the invoice, collect the money, send the notification (the callback)
return {"msg": "Invoice received"}
Hay 2 diferencias principales respecto a una path operation normal:
- No necesita tener ningún código real, porque tu aplicación nunca llamará a este código. Solo se usa para documentar la API externa. Así que, la función podría simplemente tener
pass
. - El path puede contener una expresión OpenAPI 3 (ver más abajo) donde puede usar variables con parámetros y partes del request original enviado a tu API.
La expresión del path del callback¶
El path del callback puede tener una expresión OpenAPI 3 que puede contener partes del request original enviado a tu API.
En este caso, es el str
:
"{$callback_url}/invoices/{$request.body.id}"
Entonces, si el usuario de tu API (el desarrollador externo) envía un request a tu API a:
https://yourapi.com/invoices/?callback_url=https://www.external.org/events
con un JSON body de:
{
"id": "2expen51ve",
"customer": "Mr. Richie Rich",
"total": "9999"
}
luego tu API procesará la factura, y en algún momento después, enviará un request de callback al callback_url
(la API externa):
https://www.external.org/events/invoices/2expen51ve
con un JSON body que contiene algo como:
{
"description": "Payment celebration",
"paid": true
}
y esperaría un response de esa API externa con un JSON body como:
{
"ok": true
}
Consejo
Observa cómo la URL del callback utilizada contiene la URL recibida como parámetro de query en callback_url
(https://www.external.org/events
) y también el id
de la factura desde dentro del JSON body (2expen51ve
).
Agregar el router de callback¶
En este punto tienes las path operation(s) del callback necesarias (las que el desarrollador externo debería implementar en la API externa) en el router de callback que creaste antes.
Ahora usa el parámetro callbacks
en el decorador de path operation de tu API para pasar el atributo .routes
(que en realidad es solo un list
de rutas/path operations) de ese router de callback:
from typing import Union
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Invoice(BaseModel):
id: str
title: Union[str, None] = None
customer: str
total: float
class InvoiceEvent(BaseModel):
description: str
paid: bool
class InvoiceEventReceived(BaseModel):
ok: bool
invoices_callback_router = APIRouter()
@invoices_callback_router.post(
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived
)
def invoice_notification(body: InvoiceEvent):
pass
@app.post("/invoices/", callbacks=invoices_callback_router.routes)
def create_invoice(invoice: Invoice, callback_url: Union[HttpUrl, None] = None):
"""
Create an invoice.
This will (let's imagine) let the API user (some external developer) create an
invoice.
And this path operation will:
* Send the invoice to the client.
* Collect the money from the client.
* Send a notification back to the API user (the external developer), as a callback.
* At this point is that the API will somehow send a POST request to the
external API with the notification of the invoice event
(e.g. "payment successful").
"""
# Send the invoice, collect the money, send the notification (the callback)
return {"msg": "Invoice received"}
Consejo
Observa que no estás pasando el router en sí (invoices_callback_router
) a callback=
, sino el atributo .routes
, como en invoices_callback_router.routes
.
Revisa la documentación¶
Ahora puedes iniciar tu aplicación e ir a http://127.0.0.1:8000/docs.
Verás tu documentación incluyendo una sección de "Callbacks" para tu path operation que muestra cómo debería verse la API externa: