Bloque II · Módulo 2 · Unidad 2.1
Entorno, aplicación mínima, Uvicorn y flujo de una petición
Crear un proyecto reproducible, ejecutar FastAPI en desarrollo y seguir una petición desde Uvicorn hasta una operación de ruta.
Objetivo y frontera#
En este bloque ya escribimos FastAPI, pero todavía no construimos una arquitectura completa. La meta es dominar la frontera HTTP de una aplicación pequeña: cómo arranca, cómo encuentra una operación, cómo valida y cómo forma una respuesta. Todo el estado será temporal y vivirá en memoria.
Al terminar deberías poder crear un entorno aislado, explicar main:app, distinguir recarga de desarrollo y ejecución normal, predecir qué ruta coincide y localizar en qué frontera se produce un fallo.
1. Entorno reproducible#
Un proyecto reproducible declara sus dependencias y las instala en un entorno aislado. Con Python 3.10 o superior y uv:
uv init expediente-api --bare
cd expediente-api
uv add "fastapi[standard-no-fastapi-cloud-cli]"
uv add registra la dependencia en pyproject.toml, crea el entorno cuando hace falta y mantiene un archivo de bloqueo. Una alternativa válida es crear un venv e instalar fastapi[standard] con pip; no mezcles gestores en el mismo ejercicio.
[project]
name = "expediente-api"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["fastapi[standard-no-fastapi-cloud-cli]"]
[tool.fastapi]
entrypoint = "app.main:app"
2. Aplicación mínima#
from fastapi import FastAPI
app = FastAPI(
title="Expedientes internos",
version="0.1.0",
)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
La línea app = FastAPI(...) crea la aplicación. El decorador registra que health atiende GET /health. La función no se ejecuta al importar el módulo: FastAPI conserva metadatos para invocarla cuando llegue una petición compatible.
Ejecuta:
uv run fastapi dev
El modo dev activa recarga y muestra información útil para desarrollo. La documentación interactiva aparece normalmente en /docs, la alternativa ReDoc en /redoc y el documento OpenAPI en /openapi.json.
3. Uvicorn y ASGI#
La forma app.main:app se lee de izquierda a derecha:
- importa el módulo Python
app.main; - busca en él el atributo
app; - espera que ese objeto sea una aplicación ASGI.
La forma equivalente con Uvicorn es:
uv run uvicorn app.main:app --reload
Si estás dentro del directorio equivocado, el paquete no existe, falta un __init__.py necesario para tu estructura o el atributo tiene otro nombre, el servidor no puede importar la aplicación. Eso es un error de arranque, no un 404.
| Señal | Frontera probable | Primera evidencia |
|---|---|---|
ModuleNotFoundError | Importación | Directorio actual, ruta de módulo y traceback. |
| “attribute app not found” | Objeto ASGI | Nombre exportado por el módulo. |
404 Not Found | Enrutado | Método y ruta registrados en /openapi.json. |
405 Method Not Allowed | Método HTTP | Misma ruta, método diferente. |
422 | Contrato de entrada | Cuerpo de error y ubicación loc. |
4. Flujo de una petición#
El cliente envía una petición al proceso servidor HTTP, que la entrega a la aplicación. La aplicación procesa el recurso y devuelve el resultado al proceso servidor, que construye la respuesta para el cliente.
Para GET /health, el recorrido conceptual es:
- el cliente abre o reutiliza una conexión y envía el mensaje HTTP;
- Uvicorn traduce la interacción al protocolo ASGI;
- Starlette/FastAPI compara método y ruta;
- FastAPI extrae y valida entradas declaradas;
- llama a la operación de ruta;
- serializa y valida la salida cuando existe un contrato de respuesta;
- Uvicorn envía estado, headers y body al cliente.
No todas las etapas aparecen en el código de la función. Esa es precisamente la aportación del framework: ejecutar trabajo sistemático a partir de declaraciones.
5. Rutas, parámetros y orden#
@app.get("/expedientes/me")
def current_record():
return {"id": "me"}
@app.get("/expedientes/{expediente_id}")
def read_record(expediente_id: str):
return {"id": expediente_id}
Las rutas se evalúan en orden. Si declaras primero la ruta dinámica, /expedientes/me puede tratar "me" como valor de expediente_id. La ruta estática más específica debe registrarse antes.
El nombre de la función no forma parte de la URL. Sí importan el método, el patrón del decorador y los tipos declarados.
6. Primer criterio entre def y async def#
FastAPI admite ambas formas:
@app.get("/sync")
def sync_operation():
return {"mode": "sync"}
@app.get("/async")
async def async_operation():
return {"mode": "async"}
Usa async def cuando llames directamente a una biblioteca awaitable y realmente vayas a escribir await. Usa def cuando la biblioteca relevante sea síncrona. No elijas por prestigio ni conviertas llamadas bloqueantes en no bloqueantes escribiendo async.
7. Diagnóstico mínimo basado en evidencia#
Antes de editar:
pwd
uv run python -c "from app.main import app; print(type(app).__name__)"
uv run fastapi dev
Después observa terminal, curl -i http://127.0.0.1:8000/health y /openapi.json. Una importación directa separa el problema de Python del problema de red. curl -i conserva estado y headers que el navegador puede ocultar.
8. Criterios de dominio#
Puedes considerar dominada esta unidad cuando:
- creas un proyecto desde cero sin depender de un entorno global;
- explicas cada parte de
app.main:app; - diferencias error de importación,
404,405y422; - predices la coincidencia de rutas estáticas y dinámicas;
- explicas el recorrido cliente → Uvicorn → FastAPI → operación → respuesta;
- justificas
defoasync defpor la interfaz usada.
Prácticas relacionadas#
- EX-B2-01 · Reparar una importación que impide arrancar
- EX-B2-02 · Resolver el sombreado entre rutas
- EX-B2-S01 · Admisiones, parte 1
Fuentes y siguiente paso#
La Unidad 2.2 añade datos reales a ese recorrido: path, query, header, cookie, body, formulario y archivo.