Blogs / FastAPI Dependency Injection: Una Guía Completa

FastAPI Dependency Injection: Una Guía Completa

Publicado
27 de agosto de 2026
Autor
Faizan Nadeem
Etiquetas
FastAPI Python Backend Development Software Architecture
Las manos de una persona sosteniendo dos piezas de rompecabezas juntas, a punto de conectarlas
Foto de Vardan Papikyan en Unsplash

Depends se presenta en todos los tutoriales de FastAPI como “la forma de obtener el usuario actual”, y la mayoría de los desarrolladores nunca miran más allá de ese único caso de uso. Es una pena, porque Depends es un sistema de inyección de dependencias genuino: resuelve un grafo, almacena en caché los resultados por solicitud y gestiona ciclos de vida de inicialización/desmontaje. Tratarlo solo como un decorador para comprobaciones de autenticación significa perderse los patrones que realmente mantienen testeable una base de código de FastAPI en crecimiento: sesiones de base de datos que se limpian correctamente incluso cuando un handler lanza una excepción, comprobaciones de permisos compuestas a partir de comprobaciones más pequeñas en lugar de duplicadas por ruta, y valores con alcance de solicitud calculados exactamente una vez sin importar cuántas otras dependencias los necesiten.

La otra cara es que Depends es lo bastante flexible como para construir una arquitectura genuinamente mala, y la forma más común en que eso ocurre es tratar una función de endpoint como si fuera una función normal de Python a la que simplemente puedes llamar desde otro endpoint para “reutilizar” su lógica. Compila. Incluso suele funcionar en una demo. Pero también rompe todo el modelo sobre el que se construyó el sistema de dependencias de FastAPI, y es el anti-patrón principal que separa las bases de código donde Depends multiplica la productividad de aquellas donde es una fuente de errores misteriosos. Esta publicación cubre ambas mitades: los patrones que vale la pena usar deliberadamente y los anti-patrones que conviene evitar activamente.

Aprenderás:

  • Cómo FastAPI resuelve realmente un grafo de Depends, en qué orden y con qué nivel de anidación
  • Por qué las dependencias basadas en yield son la herramienta correcta para cualquier cosa que necesite un desmontaje garantizado
  • Cómo funciona la caché de dependencias por solicitud, y cuándo silenciosamente no se aplica
  • El lugar correcto para poner dependencias compartidas para que cada router pueda usarlas sin duplicar lógica
  • Por qué llamar a una función de endpoint desde otra es un mal olor de diseño, y qué hacer en su lugar
  • Dependencias basadas en clases para comprobaciones reutilizables y parametrizadas

Tabla de contenidos

  1. Conceptos básicos
  2. Cómo funciona realmente la resolución de Depends
  3. Dependencias basadas en yield y desmontaje
  4. Caché de dependencias dentro de una solicitud
  5. Dependencias compartidas bien hechas
  6. El anti-patrón: llamar a un endpoint desde otro
  7. Dependencias basadas en clases
  8. Pruebas con dependency_overrides
  9. Errores comunes
  10. Buenas prácticas en producción

Conceptos básicos

Qué es realmente Depends

Depends marca un parámetro como algo que FastAPI debería resolver por ti antes de que tu función se ejecute: llamando a otra función (o invocable), posiblemente con sus propios parámetros Depends, y pasando el resultado. Es inyección de dependencias en el mismo sentido en que se usa el término en cualquier otro framework: tu función declara qué necesita, y otra cosa es responsable de construirlo.

from fastapi import Depends

def get_query_token(token: str) -> str:
    return token

@app.get("/items")
async def read_items(token: str = Depends(get_query_token)):
    return {"token": token}

Esto parece una pequeña comodidad para una dependencia, pero se compone: get_query_token podría depender a su vez de otra cosa, y FastAPI resuelve toda la cadena, en orden, antes de que tu handler de ruta siquiera se ejecute.

Por qué esto importa más allá de la autenticación

El primer ejemplo de todos los tutoriales es get_current_user, lo que hace que Depends parezca existir únicamente para autenticación. En la práctica, el mismo mecanismo es la herramienta correcta para sesiones de base de datos, parámetros de paginación, comprobaciones de feature flags, loggers con alcance de solicitud y limitación de tasa: cualquier cosa que sea “trabajo de inicialización que un handler necesita, calculado de forma consistente y posiblemente desmontado después” es candidata a Depends, no solo una comprobación de autenticación.

Cómo funciona realmente la resolución de Depends

FastAPI construye un grafo de dependencias por solicitud recorriendo recursivamente cada parámetro Depends, resolviendo primero las hojas. Dado:

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

def get_current_user(token: str = Depends(get_query_token), db=Depends(get_db)):
    return db.query(User).filter_by(token=token).first()

@app.get("/profile")
async def profile(user=Depends(get_current_user)):
    return user

Llamar a /profile resuelve primero get_query_token y get_db (no tienen más dependencias), luego llama a get_current_user con ambos resultados y finalmente llama al handler de ruta con el user completamente resuelto. El anidamiento puede profundizarse arbitrariamente: esto es un verdadero grafo acíclico dirigido, no una lista plana. Y FastAPI resuelve cada nodo exactamente una vez por solicitud incluso si múltiples dependencias lo necesitan, que es el comportamiento de caché que se cubre más abajo.

Las dependencias sync y async pueden mezclarse libremente

Una dependencia puede ser def o async def independientemente del tipo del handler de ruta que la consume. FastAPI resuelve dependencias async def directamente en el event loop y envía dependencias def al threadpool, exactamente la misma división explicada en Why Your FastAPI Endpoint Blocks the Event Loop para los propios handlers de ruta. Un patrón común y correcto es un handler de ruta async def con una dependencia def que hace algo genuinamente síncrono —una búsqueda ligera de configuración, por ejemplo— y FastAPI maneja automáticamente el envío al hilo para esa dependencia sin necesidad de que hagas coincidir firmas a lo largo de toda la cadena. Lo único que hay que vigilar es la misma trampa de las llamadas bloqueantes: una dependencia def es segura porque va al threadpool, pero una dependencia async def que llama a algo bloqueante sin hacer await bloquea el loop exactamente igual que lo haría un handler de ruta, ya que las dependencias heredan las mismas reglas de ejecución que los handlers a los que alimentan.

Las dependencias declaradas directamente en un APIRouter (mediante dependencies=[Depends(...)]) o en la propia app se ejecutan para cada ruta bajo ellas sin aparecer en absoluto como parámetro de función; esto es útil para comprobaciones transversales como “todo este router requiere una suscripción activa” cuando el valor de retorno de la dependencia en realidad no es necesario para el handler.

Dependencias basadas en yield y desmontaje

Una dependencia que necesita limpieza —cerrar una sesión de base de datos, liberar un lock, confirmar o revertir una transacción— usa yield en lugar de return. El código después de yield se ejecuta después de que se haya generado la respuesta y, de forma crítica, se ejecuta incluso si el handler de ruta lanzó una excepción:

async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

Este es el mecanismo que hace que “una sesión por solicitud” sea fiable y correcto: el desmontaje no es algo que cada autor de rutas tenga que recordar llamar, sino que queda garantizado por la propia dependencia sin importar cómo termine el handler. El patrón también se generaliza a subdependencias: si get_db es a su vez una dependencia de get_current_user, su desmontaje sigue ejecutándose después de que todo lo que dependía de ella haya terminado, en orden inverso al de resolución; se parece más a una pila de try/finally que a una lista plana de limpiezas independientes.

Este patrón exacto es la base del enfoque de alcance de sesión tratado en profundidad en Async SQLAlchemy Sessions in FastAPI, Done Right: si aquí haces bien el desmontaje basado en yield, la mayoría de los problemas de ciclo de vida de sesión de esa publicación ni siquiera aparecen.

Caché de dependencias dentro de una solicitud

De forma predeterminada, FastAPI almacena en caché el resultado de una dependencia durante la vida de una única solicitud. Si dos dependencias diferentes declaran Depends(get_db), FastAPI llama a get_db exactamente una vez y entrega a ambas el mismo objeto de sesión, no dos separados:

async def get_db():
    print("get_db called")
    ...

def dep_a(db=Depends(get_db)): ...
def dep_b(db=Depends(get_db)): ...

@app.get("/example")
async def example(a=Depends(dep_a), b=Depends(dep_b)):
    # "get_db called" prints exactly once, not twice
    ...

Esto importa enormemente en el caso específico de las sesiones de base de datos: sin esta caché, dep_a y dep_b operarían silenciosamente sobre dos sesiones distintas dentro de la misma solicitud, lo cual es una fuente sutil de errores tipo “¿por qué no apareció mi escritura?” cuando una dependencia escribe y otra lee dentro de la misma solicitud lógica.

La caché tiene alcance de solicitud, no global, y puede desactivarse por dependencia con Depends(get_db, use_cache=False) para el caso poco frecuente en que realmente quieras una instancia nueva incluso dentro de una sola solicitud. También conviene saber que la clave de caché es el propio invocable: dos llamadas Depends(get_db) comparten una entrada de caché porque hacen referencia al mismo objeto función, pero dos funciones diferentes que casualmente hacen lo mismo internamente no comparten una entrada de caché, aunque su lógica sea idéntica.

Dependencias compartidas bien hechas

El hogar natural para dependencias usadas en múltiples dominios es un core/dependencies.py compartido, tal como se plantea en FastAPI Project Structure That Survives Growth: get_db, get_current_user y cualquier comprobación transversal de permisos pertenecen ahí, importadas hacia abajo en cada router de dominio, nunca duplicadas por router y nunca importadas lateralmente entre dominios.

# app/core/dependencies.py
from fastapi import Depends, HTTPException, status

async def get_current_user(token: str = Depends(oauth2_scheme), db=Depends(get_db)):
    user = await db.get_user_by_token(token)
    if user is None:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED)
    return user

async def require_admin(user=Depends(get_current_user)):
    if not user.is_admin:
        raise HTTPException(status.HTTP_403_FORBIDDEN)
    return user

Observa que require_admin depende de get_current_user en lugar de reimplementar la comprobación del token: esto es composición, y es el verdadero beneficio de un grafo de dependencias real. La lógica de permisos se construye sobre la lógica de identidad en vez de duplicarla y, gracias a la caché por solicitud, usar require_admin y get_current_user en la misma ruta sigue resolviendo el usuario solo una vez.

El anti-patrón: llamar a un endpoint desde otro

Un atajo tentador cuando un endpoint nuevo necesita “básicamente lo que hace otro endpoint” es importar y llamar directamente a esa función de endpoint:

# Anti-pattern — do not do this
@app.get("/orders/{id}")
async def get_order(id: int, user=Depends(get_current_user)):
    return await fetch_order(id, user)

@app.get("/orders/{id}/summary")
async def get_order_summary(id: int, user=Depends(get_current_user)):
    order = await get_order(id, user)  # calling another route handler directly
    return summarize(order)

Esto parece inofensivo —incluso funciona correctamente en muchos casos—, pero rompe el modelo de varias formas concretas. Los parámetros Depends de la función llamada no se vuelven a resolver mediante el grafo de FastAPI; simplemente estás llamando a una función de Python y pasando manualmente sus argumentos, así que cualquier caché de dependencias, manejo de errores o desmontaje basado en yield que FastAPI normalmente coordinaría se omite por completo para esa llamada interna. Si get_order lanza una HTTPException, esta se propaga como una excepción en bruto dentro del cuerpo de get_order_summary en lugar de convertirse en una respuesta como ocurriría si el cliente realmente hubiera llamado a /orders/{id}; eso significa que las dos rutas manejan el mismo error de forma distinta según por cuál hayas entrado. Y además acopla silenciosamente las firmas de capa HTTP de ambos endpoints, de modo que cambiar los parámetros de get_order para su propia ruta ahora puede romper get_order_summary de una manera que no tiene nada que ver con el enrutamiento.

La solución es el estratificado que Depends realmente está diseñado para soportar: extrae la lógica compartida a una función de servicio que no sea un handler de ruta en absoluto, y haz que ambas rutas dependan de o llamen a esa.

# app/orders/service.py
async def fetch_order(order_id: int, user) -> Order:
    order = await db.get_order(order_id)
    if order.owner_id != user.id:
        raise HTTPException(status.HTTP_403_FORBIDDEN)
    return order

# app/orders/router.py
@router.get("/{id}")
async def get_order(id: int, user=Depends(get_current_user)):
    return await fetch_order(id, user)

@router.get("/{id}/summary")
async def get_order_summary(id: int, user=Depends(get_current_user)):
    order = await fetch_order(id, user)
    return summarize(order)

Ahora ambas rutas comparten lógica real sin que ninguna dependa del contrato HTTP de la otra, y fetch_order es trivialmente testeable con pruebas unitarias sin siquiera levantar una solicitud.

Dependencias basadas en clases

Para una dependencia que necesita configuración —un límite de paginación, un scope de permiso requerido—, una clase con __call__ te da una dependencia reutilizable y parametrizada en lugar de una familia de funciones casi idénticas:

class RequirePermission:
    def __init__(self, scope: str):
        self.scope = scope

    def __call__(self, user=Depends(get_current_user)):
        if self.scope not in user.scopes:
            raise HTTPException(status.HTTP_403_FORBIDDEN)
        return user

require_orders_write = RequirePermission("orders:write")

@router.post("/orders", dependencies=[Depends(require_orders_write)])
async def create_order(payload: OrderCreate):
    ...

Esto escala mucho mejor que escribir require_orders_write_permission, require_payments_read_permission, etcétera, como funciones separadas: la parametrización vive en el constructor, y la forma orientada a FastAPI se mantiene como un único patrón de invocable consistente en cada comprobación de permisos de la app.

Las dependencias basadas en clases también pueden usar __call__ como generador, combinando parametrización con desmontaje basado en yield; esto es útil, por ejemplo, para un rate limiter que necesita liberar un token después de que termine la solicitud:

class RateLimiter:
    def __init__(self, requests_per_minute: int):
        self.limit = requests_per_minute

    async def __call__(self, user=Depends(get_current_user)):
        token = await acquire_slot(user.id, self.limit)
        try:
            yield
        finally:
            await release_slot(token)

throttle_reports = RateLimiter(requests_per_minute=5)

@router.get("/reports", dependencies=[Depends(throttle_reports)])
async def get_reports():
    ...

Cada instancia se crea una vez, en tiempo de importación, y se reutiliza en cada solicitud: los argumentos del constructor configuran la dependencia, mientras que __call__ sigue obteniendo toda la resolución por solicitud, la caché y el comportamiento de desmontaje de cualquier otra dependencia.

Pruebas con dependency_overrides

El otro gran beneficio de construir dependencias reales en lugar de incrustar lógica directamente en los handlers es app.dependency_overrides: un diccionario que FastAPI consulta antes de resolver cualquier dependencia, permitiendo que las pruebas sustituyan get_db por una base de datos de prueba, o get_current_user por un usuario de prueba fijo, sin tocar en absoluto el código de la ruta:

# tests/conftest.py
from app.main import app
from app.core.dependencies import get_db, get_current_user

async def override_get_db():
    async with TestSessionLocal() as session:
        yield session

def override_get_current_user():
    return User(id=1, email="test@example.com", is_admin=False)

app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[get_current_user] = override_get_current_user
# tests/orders/test_router.py
async def test_create_order(client):
    response = await client.post("/orders", json={"item": "widget", "qty": 2})
    assert response.status_code == 200

Sin librería de mocking, sin monkeypatching de internals, sin levantar una instancia real de Postgres ni un flujo real de OAuth solo para probar un endpoint que casualmente requiere inicio de sesión. Esta es exactamente la razón por la que el anti-patrón anterior en esta publicación importa más allá de la limpieza del código: una ruta que llama directamente a otra función de ruta no puede ejercitarse de esta manera, porque la llamada interna nunca pasa por la maquinaria de resolución de FastAPI; dependency_overrides no tiene nada que interceptar. En cambio, una ruta que depende de una función de servicio real a través de Depends es completamente testeable de forma aislada, con cada dependencia de su grafo intercambiable de manera independiente.

Los overrides también se componen del mismo modo que las propias dependencias: sobrescribir get_db lo sobrescribe para cada dependencia que dependa transitivamente de él, incluido get_current_user, sin necesidad de un override separado para cada una. Recuerda limpiar los overrides entre módulos de prueba (app.dependency_overrides.clear() en el teardown de un fixture): un override residual de un archivo de pruebas que cambia silenciosamente el comportamiento en el siguiente es una fuente común de fallos de prueba confusos y dependientes del orden.

Errores comunes

Error: poner lógica de negocio directamente en una dependencia en lugar de en un servicio. Una función Depends que consulta, transforma y devuelve un objeto de negocio totalmente procesado vuelve esa lógica invisible para cualquier cosa que no sea una ruta de FastAPI. Solución: mantén las dependencias enfocadas en la inicialización con alcance de solicitud (auth, sesiones, paginación) y delega la lógica de negocio real a funciones de servicio.

Error: asumir que toda llamada Depends(x) comparte una entrada de caché sin importar cómo se haga referencia a x. Dos funciones que hacen lo mismo pero no son el mismo objeto no comparten un slot de caché. Solución: importa exactamente el mismo invocable en todos los lugares donde quieras que se aplique la caché; no redefinas dependencias aparentemente equivalentes en múltiples sitios.

Error: olvidar que el desmontaje basado en yield no se ejecuta hasta que la respuesta se haya generado por completo. El código después de yield se ejecuta después de que el handler retorna, lo que significa que también se ejecuta después de cualquier otra dependencia más arriba en la cadena que dependa de esta. Solución: no asumas el orden del desmontaje sin trazar el grafo de dependencias real, especialmente con dependencias anidadas.

Error: llamar directamente a una función handler de ruta desde otra ruta. Como se explicó arriba, esto omite la caché, el manejo de excepciones y las garantías de desmontaje. Solución: extrae la lógica compartida a una función de servicio simple que no pertenezca a ninguna de las dos rutas.

Error: abusar de dependencies=[...] a nivel de router para cosas cuyo valor de retorno el handler realmente necesita. Si el handler necesita el objeto de usuario resuelto, ocultarlo en dependencias a nivel de router obliga a un segundo parámetro redundante Depends(get_current_user) solo para recuperar el valor. Solución: usa dependencias a nivel de router solo para comprobaciones cuyo valor de retorno nadie necesita.

Buenas prácticas en producción

  • Mantén las dependencias con alcance de solicitud y con pocos efectos secundarios. La inicialización, las comprobaciones de auth y la gestión de sesiones van aquí; la lógica de negocio de múltiples pasos no.
  • Compón comprobaciones de permisos en lugar de duplicarlas. Hacer que require_admin dependa de get_current_user es más barato y consistente que reimplementar la comprobación del token para cada nivel de permiso.
  • Empareja siempre yield con un try/finally para que el desmontaje se ejecute incluso cuando el handler lance una excepción; no confíes solo en el camino feliz.
  • Nunca llames a un handler de ruta desde otro. Si dos endpoints necesitan la misma lógica, esa lógica pertenece a una función de servicio, no a ninguno de los dos handlers.
  • Usa dependencias basadas en clases una vez que tengas más de dos o tres variantes parametrizadas de la misma comprobación. Mantiene la superficie de permisos consistente y fácil de auditar.

Cierre

Depends es un grafo de dependencias real con garantías de caché, orden y ciclo de vida, no un decorador que casualmente obtiene el usuario actual. Usado deliberadamente, es lo que mantiene consistentes la autenticación, las sesiones y las comprobaciones transversales a través de un conjunto creciente de routers sin duplicar lógica por endpoint. Usado sin cuidado —especialmente tratando un handler de ruta como una función simple a la que llamar desde otro lugar— rompe silenciosamente las mismas garantías que hacían que valiera la pena usarlo en primer lugar.

¿Tus dependencias están haciendo inicialización con alcance de solicitud, o algunas de ellas se han convertido silenciosamente en el lugar donde vive tu lógica de negocio? Si no estás seguro, dependency_overrides es una forma rápida de averiguarlo: una dependencia que no puedes sustituir limpiamente en una prueba suele ser una que ha acumulado responsabilidades que nunca estuvo destinada a tener.

Más artículos