Saltar a contenido

Dependencias

FastAPI tiene un sistema de Inyección de Dependencias muy poderoso pero intuitivo.

Está diseñado para ser muy simple de usar, y para hacer que cualquier desarrollador integre otros componentes con FastAPI de forma muy sencilla.

Qué es la "Inyección de Dependencias"

"Inyección de Dependencias" significa, en programación, que hay una manera para que tu código (en este caso, tus path operation functions) declare las cosas que necesita para funcionar y utilizar: "dependencias".

Y luego, ese sistema (en este caso FastAPI) se encargará de hacer lo que sea necesario para proporcionar a tu código esas dependencias necesarias ("inyectar" las dependencias).

Esto es muy útil cuando necesitas:

  • Tener lógica compartida (la misma lógica de código una y otra vez).
  • Compartir conexiones a bases de datos.
  • Imponer seguridad, autenticación, requisitos de roles, etc.
  • Y muchas otras cosas...

Todo esto, mientras minimizas la repetición de código.

Primeros Pasos

Veamos un ejemplo muy simple. Será tan simple que no es muy útil, por ahora.

Pero de esta manera podemos enfocarnos en cómo funciona el sistema de Inyección de Dependencias.

Crear una dependencia, o "dependable"

Primero enfoquémonos en la dependencia.

Es solo una función que puede tomar todos los mismos parámetros que una path operation function puede tomar:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
from typing import Union

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Eso es todo.

2 líneas.

Y tiene la misma forma y estructura que todas tus path operation functions.

Puedes pensar en ella como una path operation function sin el "decorador" (sin el @app.get("/some-path")).

Y puede devolver lo que quieras.

En este caso, esta dependencia espera:

  • Un parámetro de query opcional q que es un str.
  • Un parámetro de query opcional skip que es un int, y por defecto es 0.
  • Un parámetro de query opcional limit que es un int, y por defecto es 100.

Y luego solo devuelve un dict que contiene esos valores.

Información

FastAPI agregó soporte para Annotated (y comenzó a recomendarlo) en la versión 0.95.0.

Si tienes una versión anterior, obtendrás errores al intentar usar Annotated.

Asegúrate de Actualizar la versión de FastAPI al menos a la 0.95.1 antes de usar Annotated.

Importar Depends

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
from typing import Union

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Declarar la dependencia, en el "dependant"

De la misma forma en que usas Body, Query, etc. con los parámetros de tu path operation function, usa Depends con un nuevo parámetro:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons
from typing import Union

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    return commons


@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
    return commons

Tip

Prefer to use the Annotated version if possible.

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Tip

Prefer to use the Annotated version if possible.

from typing import Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons


@app.get("/users/")
async def read_users(commons: dict = Depends(common_parameters)):
    return commons

Aunque usas Depends en los parámetros de tu función de la misma manera que usas Body, Query, etc., Depends funciona un poco diferente.

Le das a Depends un solo parámetro.

Este parámetro debe ser algo como una función.

No la llames directamente (no agregues los paréntesis al final), solo pásala como un parámetro a Depends().

Y esa función toma parámetros de la misma manera que las path operation functions.

Consejo

Verás qué otras "cosas", además de funciones, pueden usarse como dependencias en el próximo capítulo.

Cada vez que llega un nuevo request, FastAPI se encargará de:

  • Llamar a tu función de dependencia ("dependable") con los parámetros correctos.
  • Obtener el resultado de tu función.
  • Asignar ese resultado al parámetro en tu path operation function.
graph TB

common_parameters(["common_parameters"])
read_items["/items/"]
read_users["/users/"]

common_parameters --> read_items
common_parameters --> read_users

De esta manera escribes código compartido una vez y FastAPI se encarga de llamarlo para tus path operations.

Revisa

Nota que no tienes que crear una clase especial y pasarla en algún lugar a FastAPI para "registrarla" o algo similar.

Solo la pasas a Depends y FastAPI sabe cómo hacer el resto.

Compartir dependencias Annotated

En los ejemplos anteriores, ves que hay un poquito de duplicación de código.

Cuando necesitas usar la dependencia common_parameters(), tienes que escribir todo el parámetro con la anotación de tipo y Depends():

commons: Annotated[dict, Depends(common_parameters)]

Pero como estamos usando Annotated, podemos almacenar ese valor Annotated en una variable y usarlo en múltiples lugares:

from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}


CommonsDep = Annotated[dict, Depends(common_parameters)]


@app.get("/items/")
async def read_items(commons: CommonsDep):
    return commons


@app.get("/users/")
async def read_users(commons: CommonsDep):
    return commons
🤓 Other versions and variants
from typing import Annotated, Union

from fastapi import Depends, FastAPI

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


CommonsDep = Annotated[dict, Depends(common_parameters)]


@app.get("/items/")
async def read_items(commons: CommonsDep):
    return commons


@app.get("/users/")
async def read_users(commons: CommonsDep):
    return commons
from typing import Union

from fastapi import Depends, FastAPI
from typing_extensions import Annotated

app = FastAPI()


async def common_parameters(
    q: Union[str, None] = None, skip: int = 0, limit: int = 100
):
    return {"q": q, "skip": skip, "limit": limit}


CommonsDep = Annotated[dict, Depends(common_parameters)]


@app.get("/items/")
async def read_items(commons: CommonsDep):
    return commons


@app.get("/users/")
async def read_users(commons: CommonsDep):
    return commons

Consejo

Esto es solo Python estándar, se llama un "alias de tipo", en realidad no es específico de FastAPI.

Pero porque FastAPI está basado en los estándares de Python, incluido Annotated, puedes usar este truco en tu código. 😎

Las dependencias seguirán funcionando como se esperaba, y la mejor parte es que la información de tipo se preservará, lo que significa que tu editor podrá seguir proporcionándote autocompletado, errores en línea, etc. Lo mismo para otras herramientas como mypy.

Esto será especialmente útil cuando lo uses en una gran base de código donde uses las mismas dependencias una y otra vez en muchas path operations.

Usar async o no usar async

Como las dependencias también serán llamadas por FastAPI (lo mismo que tus path operation functions), las mismas reglas aplican al definir tus funciones.

Puedes usar async def o def normal.

Y puedes declarar dependencias con async def dentro de path operation functions normales def, o dependencias def dentro de path operation functions async def, etc.

No importa. FastAPI sabrá qué hacer.

Nota

Si no lo sabes, revisa la sección Async: "¿Con prisa?" sobre async y await en la documentación.

Integración con OpenAPI

Todas las declaraciones de request, validaciones y requisitos de tus dependencias (y sub-dependencias) se integrarán en el mismo esquema de OpenAPI.

Así, la documentación interactiva tendrá toda la información de estas dependencias también:

Uso simple

Si lo ves, las path operation functions se declaran para ser usadas siempre que un path y una operación coincidan, y luego FastAPI se encarga de llamar la función con los parámetros correctos, extrayendo los datos del request.

En realidad, todos (o la mayoría) de los frameworks web funcionan de esta misma manera.

Nunca llamas directamente a esas funciones. Son llamadas por tu framework (en este caso, FastAPI).

Con el sistema de Inyección de Dependencias, también puedes decirle a FastAPI que tu path operation function también "depende" de algo más que debe ejecutarse antes que tu path operation function, y FastAPI se encargará de ejecutarlo e "inyectar" los resultados.

Otros términos comunes para esta misma idea de "inyección de dependencias" son:

  • recursos
  • proveedores
  • servicios
  • inyectables
  • componentes

Plug-ins de FastAPI

Las integraciones y "plug-ins" pueden construirse usando el sistema de Inyección de Dependencias. Pero, de hecho, en realidad no hay necesidad de crear "plug-ins", ya que al usar dependencias es posible declarar una cantidad infinita de integraciones e interacciones que se vuelven disponibles para tus path operation functions.

Y las dependencias se pueden crear de una manera muy simple e intuitiva que te permite simplemente importar los paquetes de Python que necesitas, e integrarlos con tus funciones de API en un par de líneas de código, literalmente.

Verás ejemplos de esto en los próximos capítulos, sobre bases de datos relacionales y NoSQL, seguridad, etc.

Compatibilidad de FastAPI

La simplicidad del sistema de inyección de dependencias hace que FastAPI sea compatible con:

  • todas las bases de datos relacionales
  • bases de datos NoSQL
  • paquetes externos
  • APIs externas
  • sistemas de autenticación y autorización
  • sistemas de monitoreo de uso de la API
  • sistemas de inyección de datos de response
  • etc.

Simple y Poderoso

Aunque el sistema de inyección de dependencias jerárquico es muy simple de definir y usar, sigue siendo muy poderoso.

Puedes definir dependencias que a su vez pueden definir dependencias ellas mismas.

Al final, se construye un árbol jerárquico de dependencias, y el sistema de Inyección de Dependencias se encarga de resolver todas estas dependencias por ti (y sus sub-dependencias) y proporcionar (inyectar) los resultados en cada paso.

Por ejemplo, digamos que tienes 4 endpoints de API (path operations):

  • /items/public/
  • /items/private/
  • /users/{user_id}/activate
  • /items/pro/

entonces podrías agregar diferentes requisitos de permiso para cada uno de ellos solo con dependencias y sub-dependencias:

graph TB

current_user(["current_user"])
active_user(["active_user"])
admin_user(["admin_user"])
paying_user(["paying_user"])

public["/items/public/"]
private["/items/private/"]
activate_user["/users/{user_id}/activate"]
pro_items["/items/pro/"]

current_user --> active_user
active_user --> admin_user
active_user --> paying_user

current_user --> public
active_user --> private
admin_user --> activate_user
paying_user --> pro_items

Integrado con OpenAPI

Todas estas dependencias, al declarar sus requisitos, también añaden parámetros, validaciones, etc. a tus path operations.

FastAPI se encargará de agregar todo al esquema de OpenAPI, para que se muestre en los sistemas de documentación interactiva.