En esta página

Introducción a FastAPI

14 min lectura TextoCap. 5 — Python para web

¿Qué es FastAPI?

FastAPI es un framework web moderno para construir APIs en Python. Fue creado por Sebastián Ramírez y publicado en 2018. Rápidamente se convirtió en uno de los frameworks más populares de Python gracias a tres características excepcionales:

  1. Velocidad: Comparable con Node.js y Go gracias a Starlette y Uvicorn (ASGI)
  2. Validación automática: Usa Pydantic para validar datos de entrada y salida
  3. Documentación automática: Genera Swagger UI y ReDoc sin escribir una línea extra

FastAPI es el framework elegido por empresas como Microsoft, Uber, Netflix y decenas de startups para sus servicios internos.

Instalación

# FastAPI y Uvicorn (servidor ASGI)
pip install fastapi uvicorn[standard]

# Pydantic ya viene incluido con FastAPI
# Para desarrollo también instala:
pip install python-multipart httpx  # Para formularios y tests

Tu primera aplicación

# archivo: main.py
from fastapi import FastAPI

app = FastAPI(
    title="Mi Primera API",
    description="Una API de ejemplo con FastAPI",
    version="0.1.0"
)

@app.get("/")
async def raíz() -> dict:
    return {"mensaje": "¡Hola desde FastAPI!", "version": "0.1.0"}

@app.get("/salud")
async def verificar_salud() -> dict:
    return {"estado": "saludable", "servicio": "Mi API"}

Para ejecutar:

uvicorn main:app --reload
# INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
# INFO:     Started reloader process using StatReload

Ahora visita:

  • http://localhost:8000/ — Tu endpoint raíz
  • http://localhost:8000/docs — Swagger UI interactivo
  • http://localhost:8000/redoc — Documentación ReDoc

Parámetros de ruta

Los parámetros de ruta se definen con llaves {} en el decorador:

from fastapi import FastAPI, Path

app = FastAPI()

productos = {
    1: {"nombre": "Laptop", "precio": 1299.99},
    2: {"nombre": "Mouse", "precio": 29.99},
    3: {"nombre": "Teclado", "precio": 89.99},
}

@app.get("/productos/{producto_id}")
async def obtener_producto(
    producto_id: int = Path(..., ge=1, description="ID del producto")
) -> dict:
    if producto_id not in productos:
        from fastapi import HTTPException
        raise HTTPException(status_code=404, detail=f"Producto {producto_id} no encontrado")
    return productos[producto_id]

@app.get("/usuarios/{usuario_id}/posts/{post_id}")
async def obtener_post_de_usuario(usuario_id: int, post_id: int) -> dict:
    return {"usuario_id": usuario_id, "post_id": post_id}

Parámetros de consulta (query params)

Los parámetros de consulta son los que van después del ? en la URL:

from fastapi import FastAPI, Query

@app.get("/productos")
async def listar_productos(
    pagina: int = Query(1, ge=1, description="Número de página"),
    limite: int = Query(10, ge=1, le=100, description="Elementos por página"),
    buscar: str | None = Query(None, min_length=2, description="Texto de búsqueda"),
    ordenar_por: str = Query("nombre", regex="^(nombre|precio)$"),
) -> dict:
    todos = list(productos.values())

    if buscar:
        todos = [p for p in todos if buscar.lower() in p["nombre"].lower()]

    inicio = (pagina - 1) * limite
    fin = inicio + limite

    return {
        "total": len(todos),
        "pagina": pagina,
        "limite": limite,
        "datos": todos[inicio:fin]
    }

Modelos Pydantic para el body de la petición

Pydantic es la columna vertebral de la validación en FastAPI:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, EmailStr, validator
from typing import Annotated
from datetime import datetime

# Modelo para crear (entrada)
class UsuarioCrear(BaseModel):
    nombre: str = Field(..., min_length=2, max_length=100, description="Nombre completo")
    email: EmailStr = Field(..., description="Email válido")
    edad: int = Field(..., ge=13, le=120, description="Edad en años")
    sitio_web: str | None = Field(None, description="URL opcional del sitio web")

    # Validador personalizado (Pydantic v2)
    @validator("nombre")
    def nombre_no_puede_ser_solo_espacios(cls, v: str) -> str:
        if v.strip() == "":
            raise ValueError("El nombre no puede ser solo espacios")
        return v.strip()

# Modelo de respuesta (salida) — puede tener más campos
class Usuario(UsuarioCrear):
    id: int
    activo: bool = True
    creado_en: datetime = Field(default_factory=datetime.now)

    class Config:
        # En Pydantic v2 esto cambia a model_config = ConfigDict(...)
        json_schema_extra = {
            "example": {
                "nombre": "Ana García",
                "email": "[email protected]",
                "edad": 28,
                "sitio_web": "https://ana.dev"
            }
        }


# Base de datos simulada
usuarios_db: dict[int, Usuario] = {}
id_counter = 0

@app.post("/usuarios", response_model=Usuario, status_code=201)
async def crear_usuario(datos: UsuarioCrear) -> Usuario:
    global id_counter

    # Verificar email único
    for u in usuarios_db.values():
        if u.email == datos.email:
            raise HTTPException(
                status_code=409,
                detail=f"Ya existe un usuario con el email {datos.email}"
            )

    id_counter += 1
    usuario = Usuario(id=id_counter, **datos.model_dump())
    usuarios_db[id_counter] = usuario
    return usuario

@app.get("/usuarios", response_model=list[Usuario])
async def listar_usuarios(activo: bool | None = None) -> list[Usuario]:
    usuarios = list(usuarios_db.values())
    if activo is not None:
        usuarios = [u for u in usuarios if u.activo == activo]
    return usuarios

@app.get("/usuarios/{usuario_id}", response_model=Usuario)
async def obtener_usuario(usuario_id: int) -> Usuario:
    if usuario_id not in usuarios_db:
        raise HTTPException(status_code=404, detail="Usuario no encontrado")
    return usuarios_db[usuario_id]

@app.put("/usuarios/{usuario_id}", response_model=Usuario)
async def actualizar_usuario(usuario_id: int, datos: UsuarioCrear) -> Usuario:
    if usuario_id not in usuarios_db:
        raise HTTPException(status_code=404, detail="Usuario no encontrado")
    usuario_actualizado = Usuario(id=usuario_id, **datos.model_dump())
    usuarios_db[usuario_id] = usuario_actualizado
    return usuario_actualizado

@app.delete("/usuarios/{usuario_id}", status_code=204)
async def eliminar_usuario(usuario_id: int) -> None:
    if usuario_id not in usuarios_db:
        raise HTTPException(status_code=404, detail="Usuario no encontrado")
    del usuarios_db[usuario_id]

Manejo de errores con HTTPException

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

# Manejador global de excepciones personalizadas
class ErrorDeNegocio(Exception):
    def __init__(self, mensaje: str, codigo: int = 400) -> None:
        self.mensaje = mensaje
        self.codigo = codigo

@app.exception_handler(ErrorDeNegocio)
async def manejar_error_negocio(request: Request, exc: ErrorDeNegocio) -> JSONResponse:
    return JSONResponse(
        status_code=exc.codigo,
        content={"error": exc.mensaje, "tipo": "error_negocio"}
    )

@app.get("/operacion-arriesgada")
async def operacion_arriesgada(valor: int) -> dict:
    if valor < 0:
        raise ErrorDeNegocio("El valor no puede ser negativo", codigo=400)
    if valor > 1000:
        raise HTTPException(
            status_code=422,
            detail={
                "mensaje": "Valor demasiado grande",
                "maximo": 1000,
                "recibido": valor
            }
        )
    return {"resultado": valor * 2}

Middleware y dependencias

from fastapi import FastAPI, Depends, Header
from typing import Annotated

# Dependencia para autenticación
async def verificar_token(
    authorization: Annotated[str | None, Header()] = None
) -> str:
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(
            status_code=401,
            detail="Token de autorización requerido",
            headers={"WWW-Authenticate": "Bearer"}
        )
    token = authorization.removeprefix("Bearer ")
    if token != "mi-token-secreto":
        raise HTTPException(status_code=403, detail="Token inválido")
    return token

@app.get("/ruta-protegida")
async def ruta_protegida(token: Annotated[str, Depends(verificar_token)]) -> dict:
    return {"mensaje": "Acceso concedido", "token": token[:10] + "..."}

Eventos de inicio y cierre

from contextlib import asynccontextmanager
from typing import AsyncGenerator

@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator:
    # Código que se ejecuta al INICIO
    print("Iniciando la aplicación...")
    # Aquí conectarías a la base de datos, cargarías modelos ML, etc.

    yield  # La aplicación corre aquí

    # Código que se ejecuta al CIERRE
    print("Cerrando la aplicación...")
    # Aquí cerrarías conexiones, guardarías estado, etc.


app = FastAPI(lifespan=lifespan)

Estructura de proyecto recomendada

Para proyectos reales, organiza tu FastAPI así:

mi_api/
├── main.py           # Punto de entrada
├── routers/
│   ├── __init__.py
│   ├── usuarios.py   # APIRouter de usuarios
│   └── productos.py  # APIRouter de productos
├── models/
│   ├── __init__.py
│   └── usuario.py    # Modelos Pydantic
├── services/
│   └── usuario_service.py  # Lógica de negocio
└── dependencies.py   # Dependencias compartidas
# routers/usuarios.py
from fastapi import APIRouter

router = APIRouter(prefix="/usuarios", tags=["Usuarios"])

@router.get("/")
async def listar() -> list: ...

@router.get("/{uid}")
async def obtener(uid: int) -> dict: ...

# main.py
from fastapi import FastAPI
from routers import usuarios, productos

app = FastAPI()
app.include_router(usuarios.router)
app.include_router(productos.router)

Resumen

FastAPI es uno de los frameworks más modernos y eficientes para construir APIs en Python. Combina la velocidad de Starlette/ASGI con la validación automática de Pydantic y documentación Swagger sin esfuerzo adicional. En la próxima lección profundizaremos en los decoradores y el sistema de type hints de Python.

FastAPI genera documentación interactiva automáticamente
Al ejecutar tu API, FastAPI genera Swagger UI en /docs y ReDoc en /redoc. Puedes probar todos los endpoints directamente desde el navegador sin ninguna configuración adicional.
Pydantic v2 valida automáticamente en tiempo de ejecución
Los modelos Pydantic validan que los datos entrantes tengan el tipo y formato correcto. Si el cliente envía un campo inválido, FastAPI devuelve automáticamente un error 422 (Unprocessable Entity) con detalles de qué falló.
Usa async def para endpoints que hacen I/O
Define tus endpoints con async def cuando hacen operaciones de I/O (base de datos, HTTP a otras APIs, archivos). Para operaciones puramente CPU-intensivas sin I/O, usa def regular para evitar bloquear el event loop.