Paso 6 · Bloque III · Unidad 3.2
Comparte recursos y configura cada entorno
Construye árboles de dependencias explícitos, diferencia caché por petición y por proceso, valida settings externos y prueba el ciclo de vida de recursos con yield.
1. Predice el árbol antes de añadir Depends#
La Unidad 3.1 dejó un punto de composición y routers con un contrato estable. Ahora aparece otra repetición: varias operaciones necesitan el mismo contexto de petición, la misma configuración o un recurso que debe cerrarse. Copiar ese trabajo en cada endpoint oculta diferencias y hace difícil sustituir una frontera durante una prueba.
Antes de escribir Depends, dibuja el requisito desde la operación hacia sus proveedores:
PATCH /requests/{id}/decision
├── request context
│ ├── X-Correlation-Id
│ └── X-Organization-Id
├── settings
└── audit sink
└── settings
Predice cuatro hechos:
- qué entrada HTTP declara cada nodo;
- qué valor entrega al siguiente;
- cuántas veces debe ejecutarse dentro de una petición;
- qué acción debe ocurrir después de usar el recurso.
Esta representación evita dos extremos: repetir lógica de frontera en cada ruta y convertir un contenedor global opaco en la arquitectura de la aplicación.
2. Una dependencia es un proveedor con firma pública#
Una dependencia puede recibir path, query, headers, cookies o body igual que una operación. FastAPI incorpora esas declaraciones a validación y OpenAPI. Por eso extraer una entrada común no debe volverla invisible.
from typing import Annotated
from fastapi import Depends, Header, Query
from pydantic import BaseModel, Field
class RequestContext(BaseModel):
correlation_id: str
organization_id: str
page_size: int
def read_correlation_id(
x_correlation_id: Annotated[str, Header(min_length=8)],
) -> str:
return x_correlation_id
def build_request_context(
correlation_id: Annotated[str, Depends(read_correlation_id)],
organization_id: Annotated[str, Header(alias="X-Organization-Id")],
page_size: Annotated[int, Query(ge=1, le=100)] = 25,
) -> RequestContext:
return RequestContext(
correlation_id=correlation_id,
organization_id=organization_id,
page_size=page_size,
)
RequestContextDep = Annotated[RequestContext, Depends(build_request_context)]
El alias conserva tipo y metadatos sin repetir Annotated[...]. Una operación expresa su requisito en la firma:
@router.get("")
def list_requests(context: RequestContextDep) -> list[ComplianceRequestPublic]:
return find_requests(
organization_id=context.organization_id,
limit=context.page_size,
)
En /docs deben seguir apareciendo X-Correlation-Id, X-Organization-Id y page_size. Si desaparecen al refactorizar, no has ocultado solo implementación: has roto la descripción pública de la petición.
3. FastAPI resuelve primero las subdependencias#
build_request_context necesita read_correlation_id; una operación necesita build_request_context. FastAPI recorre ese grafo desde las hojas y entrega cada resultado al nodo que lo declaró.
read_correlation_id ──► build_request_context ──► list_requests
Si dos ramas usan la misma dependencia durante una petición, FastAPI reutiliza por defecto su resultado. Esa caché pertenece a esa petición:
┌─► build_request_context ─┐
read_correlation ┤ ├─► operation
└─► audit_context ─────────┘
read_correlation_id se ejecuta una vez y ambas ramas reciben el valor cacheado. use_cache=False fuerza otra ejecución, pero es una excepción que exige un motivo observable; no es el remedio para un proveedor con estado mutable mal diseñado.
Una dependencia puede ser síncrona o asíncrona. La elección sigue el mismo criterio que para una operación: la presencia de Depends no convierte una función bloqueante en no bloqueante. El modelo temporal completo se recuperará en la Unidad 6.2.
4. Separa tres relojes distintos#
La palabra “caché” induce errores cuando no se nombra su alcance. En esta unidad conviven tres mecanismos diferentes:
| Mecanismo | Duración | Uso | Prueba decisiva |
|---|---|---|---|
Caché de Depends | Una petición | Reutilizar un nodo del grafo. | Una ejecución en una petición; nueva ejecución en la siguiente. |
lru_cache de settings | Proceso Python | No releer fuentes estables en cada petición. | Misma instancia en llamadas sucesivas del proceso; override o limpieza controlada en tests. |
Dependencia con yield | Alrededor de la operación o del ciclo request/response | Adquirir y liberar un recurso. | Orden de apertura, uso y cierre en éxito y error. |
Un deployment con varios workers tiene varios procesos y, por tanto, una caché de settings por worker. lru_cache no sincroniza procesos, no recarga cambios del entorno y no almacena secretos de forma segura. Solo evita reconstruir el objeto en ese proceso.
5. Configuración tipada: entrada externa, no constantes dispersas#
Configuración es aquello que puede cambiar entre despliegues sin modificar el código: nombre operativo, URL de una integración, límites o identificadores de entorno. Los datos de una petición pertenecen al contrato HTTP; las reglas estables pertenecen al código. Un secreto es configuración sensible, pero su almacenamiento y entrega requieren además un mecanismo autorizado del despliegue.
Pydantic 2 separa BaseSettings en el paquete pydantic-settings:
# app/config.py
from functools import lru_cache
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="COMPLIANCE_",
env_file=".env",
extra="ignore",
)
app_name: str = "Compliance API"
audit_channel: str
max_page_size: int = Field(default=100, ge=1, le=500)
@lru_cache
def get_settings() -> Settings:
return Settings()
COMPLIANCE_AUDIT_CHANNEL es obligatorio; si falta, construir Settings produce un error de validación en lugar de dejar un valor incompleto circular por la aplicación. Las variables de entorno tienen prioridad sobre valores dotenv en la configuración estándar documentada. Si personalizas las fuentes, vuelve a documentar y probar la precedencia: deja de ser obvia.
from typing import Annotated
from fastapi import Depends
from app.config import Settings, get_settings
SettingsDep = Annotated[Settings, Depends(get_settings)]
El objeto se crea en la primera llamada al proveedor y luego se conserva en ese proceso. Si necesitas que un despliegue falle antes de aceptar tráfico, valida get_settings() durante la construcción o el ciclo de arranque de la aplicación y prueba ese arranque. “Existe un modelo tipado” no demuestra por sí solo fail fast.
6. yield hace observable el ciclo de vida de un recurso#
Algunos proveedores no solo construyen un valor: deben cerrarlo. El código anterior a yield adquiere el recurso; el valor cedido llega a la operación; finally garantiza la liberación.
from collections.abc import Iterator
from typing import Annotated
from fastapi import Depends
class AuditSink:
def __init__(self, channel: str) -> None:
self.channel = channel
def write(self, event: str) -> None:
...
def close(self) -> None:
...
def get_audit_sink(settings: SettingsDep) -> Iterator[AuditSink]:
sink = AuditSink(channel=settings.audit_channel)
try:
yield sink
finally:
sink.close()
AuditSinkDep = Annotated[AuditSink, Depends(get_audit_sink)]
El árbol y la línea temporal se leen juntos:
get_settings ─► get_audit_sink ─► decide_request
abrir usar
└──────────────── cerrar en finally
Por defecto, una dependencia con yield usa alcance de request y ejecuta su salida después de enviar la respuesta. La API actual permite scope="function" cuando el recurso debe cerrarse después de retornar la operación y antes de enviar la respuesta. No cambies el scope por intuición: demuestra qué código necesita todavía el recurso, incluida la serialización o una subdependencia.
Si capturas una excepción después de yield, vuelve a lanzarla salvo que la traduzcas deliberadamente a otra excepción. Silenciarla puede ocultar un fallo que FastAPI ya no podrá observar.
7. Sustituye el proveedor, no la ruta que quieres probar#
FastAPI consulta app.dependency_overrides al resolver el grafo. La clave es el callable original y el valor es el proveedor sustituto:
from fastapi.testclient import TestClient
from app.config import Settings, get_settings
from app.main import app
def test_uses_test_settings() -> None:
def override_settings() -> Settings:
return Settings(
app_name="Compliance test",
audit_channel="memory",
_env_file=None,
)
app.dependency_overrides[get_settings] = override_settings
try:
with TestClient(app) as client:
response = client.get("/runtime")
assert response.json()["app_name"] == "Compliance test"
finally:
app.dependency_overrides.clear()
Routing, validación y serialización siguen siendo reales. Solo cambia la frontera externa o no determinista. El finally evita que el override contamine otra prueba. Una fixture puede centralizar esa instalación y limpieza cuando el patrón se repite.
8. Elige el menor alcance que expresa el requisito#
Una dependencia con valor consumido pertenece a la firma de la operación o de otra dependencia. Cuando solo valida o produce un efecto, puede declararse en dependencies=[Depends(...)] en una operación, un router o toda la aplicación.
| Alcance | Cuándo ayuda | Riesgo principal |
|---|---|---|
| Parámetro de operación | La función usa el valor y debe mostrar su requisito. | Firmas largas si no existe una frontera coherente. |
| Decorador de operación | Se necesita el efecto, no el valor. | Ocultar qué produjo el efecto si el nombre no es claro. |
| Router | Todas las operaciones del grupo comparten una política real. | Aplicarla por simetría a rutas que no la necesitan. |
| Aplicación | Todo request debe satisfacer el mismo requisito transversal. | Requisito invisible y acoplamiento global difícil de exceptuar. |
Usa el alcance más pequeño correcto. Un header de organización necesario solo para solicitudes de cumplimiento no debería convertirse en dependencia global de /health. Settings pueden ser reutilizados ampliamente, pero eso no obliga a inyectarlos en cada endpoint si solo otro proveedor los consume.
Depends tampoco es un service locator: una operación no debería pedir un contenedor genérico y buscar dentro nombres dinámicos. Las dependencias concretas mantienen el grafo inspeccionable, tipado y sustituible.
9. Diagnostica la primera frontera que contradice la evidencia#
| Observación | Hipótesis prioritaria | Comprobación corta |
|---|---|---|
| El parámetro dejó de aparecer en OpenAPI. | La dependencia ya no declara esa entrada o quedó fuera del grafo. | Inspecciona la firma del callable y app.openapi(). |
| Un contador aumenta dos veces en la misma petición. | Se usó use_cache=False, callables distintos o ejecución manual. | Registra identidad del proveedor y ramas que lo declaran. |
| Cambiar el entorno no cambia settings. | lru_cache conserva la instancia del proceso. | Sustituye get_settings o limpia su caché en un test aislado. |
| Un test afecta al siguiente. | Override, caché o recurso mutable no restaurado. | Comprueba el finally de la fixture y el mapa de overrides. |
| El recurso queda abierto después de un 404/500. | Falta finally o el objeto se creó fuera de la dependencia con yield. | Registra open/use/close en éxito y error. |
/health exige un header de negocio. | Dependencia aplicada con alcance global demasiado amplio. | Revisa la operación, el router y FastAPI(dependencies=…). |
10. Decisiones y límites de esta unidad#
Esta unidad introduce dependencias para entrada compartida, settings tipados, caché por petición, caché de proceso, overrides y recursos simulados con yield. No introduce sesiones de base de datos, autenticación, autorización, clientes HTTP reales, transacciones, lifespan, gestores de secretos ni contenedores de inyección externos.
Tampoco convierte cada helper en dependencia. Una función pura que recibe argumentos normales y no necesita que FastAPI la resuelva puede seguir siendo una función. Las reglas de dominio no ganan calidad por importar Depends; mantenerlas independientes del framework puede hacerlas más fáciles de probar.
La condición de parada exige:
- un árbol pequeño que puedas dibujar y justificar;
- parámetros de frontera todavía visibles en OpenAPI;
- configuración validada desde fuentes externas, sin valores sensibles publicados;
- diferencia explícita entre caché por petición y por proceso;
- cleanup probado en éxito y error;
- overrides restaurados después de cada prueba;
- ninguna capa, sesión o mecanismo de seguridad anticipado.
11. Criterios de dominio#
- Dibujas el orden de resolución de una operación con dos niveles de subdependencias.
- Predices cuándo un proveedor se ejecuta una vez por petición y cuándo persiste por proceso.
- Mantienes parámetros y validaciones de una dependencia visibles en OpenAPI.
- Distingues configuración, datos de usuario, reglas estables y secretos.
- Explicas cuándo ocurre el cleanup por defecto y demuestras liberación en éxito y error.
- Eliges operación, router o aplicación según el menor alcance correcto.
- Sustituyes settings o un recurso durante una prueba y restauras el estado global del test.
- Rechazas usar
Dependscomo justificación de una arquitectura universal. - Transfieres el mecanismo a una API modular distinta sin copiar el árbol del ejemplo.
Prácticas relacionadas#
- EX-B3-03 · Construir y observar un árbol de dependencias
- EX-B3-04 · Validar settings y sustituirlos en tests
- EX-B3-09 · Reconstruir el ciclo de vida de un recurso
- EX-B3-S01-02 · Checkpoint: proporcionar contexto, settings y auditoría