IF0100 - Programacion OO II

Unidad 5: Arquitectura de Datos Desconectados

Clase 4: API REST Avanzada

Jueves, 04 de junio de 2026 Semana 18 - Jueves (60 minutos) 30 min Teoría 30 min Práctica

Curso completado

Objetivos de Aprendizaje

Al finalizar esta clase, seras capaz de:

  • Implementar paginacion en endpoints
  • Crear filtros y busqueda avanzada
  • Manejar errores HTTP correctamente
  • Implementar versionado de API
  • Documentar API con OpenAPI

Paginacion (20 min)

from fastapi import Query
from typing import Generic, TypeVar, List
from pydantic import BaseModel

T = TypeVar("T")

class Paginado(BaseModel, Generic[T]):
    items: List[T]
    total: int
    pagina: int
    por_pagina: int
    total_paginas: int

def calcular_offset(pagina: int, por_pagina: int) -> int:
    return (pagina - 1) * por_pagina

@router.get("/tareas", response_model=Paginado[TareaResponse])
def listar_tareas(
    pagina: int = Query(1, ge=1),
    por_pagina: int = Query(10, ge=1, le=100),
    repo: TareaRepositorio = Depends(get_tarea_repo)
):
    # Calcular offset
    offset = calcular_offset(pagina, por_pagina)
    
    # Obtener datos
    tareas = repo.obtener_todos(skip=offset, limit=por_pagina)
    total = repo.contar()
    
    # Calcular total paginas
    total_paginas = (total + por_pagina - 1) // por_pagina
    
    return Paginado(
        items=[TareaResponse.from_entity(t) for t in tareas],
        total=total,
        pagina=pagina,
        por_pagina=por_pagina,
        total_paginas=total_paginas
    )

Filtros y Busqueda (20 min)

from fastapi import Query
from typing import Optional
from enum import Enum
from datetime import datetime

class OrdenDireccion(str, Enum):
    ASC = "asc"
    DESC = "desc"

class TareaFiltro(BaseModel):
    """Filtros para tareas."""
    completada: Optional[bool] = None
    prioridad: Optional[Prioridad] = None
    proyecto_id: Optional[int] = None
    buscar: Optional[str] = None
    fecha_desde: Optional[datetime] = None
    fecha_hasta: Optional[datetime] = None
    ordenar_por: str = "creado_en"
    orden_dir: OrdenDireccion = OrdenDireccion.DESC

@router.get("/tareas")
def listar_tareas(
    completada: Optional[bool] = Query(None),
    prioridad: Optional[Prioridad] = Query(None),
    proyecto_id: Optional[int] = Query(None),
    buscar: Optional[str] = Query(None, min_length=2),
    pagina: int = Query(1, ge=1),
    por_pagina: int = Query(10, ge=1, le=100),
    repo: TareaRepositorio = Depends(get_tarea_repo)
):
    # Construir query
    query = repo.db.query(Tarea)
    
    # Aplicar filtros
    if completada is not None:
        query = query.filter(Tarea.completada == completada)
    
    if prioridad:
        query = query.filter(Tarea.prioridad == prioridad)
    
    if proyecto_id:
        query = query.filter(Tarea.proyecto_id == proyecto_id)
    
    if buscar:
        query = query.filter(
            Tarea.titulo.ilike(f"%{buscar}%") |
            Tarea.descripcion.ilike(f"%{buscar}%")
        )
    
    # Contar total
    total = query.count()
    
    # Aplicar paginacion
    offset = calcular_offset(pagina, por_pagina)
    tareas = query.offset(offset).limit(por_pagina).all()
    
    return construir_respuesta_paginada(tareas, total, pagina, por_pagina)

Manejo de Errores (15 min)

from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

# Excepciones personalizadas
class AppException(Exception):
    def __init__(self, status_code: int, mensaje: str, detalle: dict = None):
        self.status_code = status_code
        self.mensaje = mensaje
        self.detalle = detalle or {}

class NoEncontradoException(AppException):
    def __init__(self, recurso: str, id: int):
        super().__init__(
            status_code=404,
            mensaje=f"{recurso} no encontrado",
            detalle={"id": id}
        )

class ConflictoException(AppException):
    def __init__(self, mensaje: str):
        super().__init__(status_code=409, mensaje=mensaje)

# Handler global
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": True,
            "mensaje": exc.mensaje,
            "detalle": exc.detalle
        }
    )

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content={
            "error": True,
            "mensaje": "Datos de entrada invalidos",
            "errores": exc.errors()
        }
    )

# Uso en endpoints
@router.get("/tareas/{id}")
def obtener_tarea(id: int, repo: TareaRepositorio = Depends(get_tarea_repo)):
    tarea = repo.obtener_por_id(id)
    if not tarea:
        raise NoEncontradoException("Tarea", id)
    return TareaResponse.from_entity(tarea)

Versionado de API (15 min)

from fastapi import APIRouter

# Estrategia: URL path versioning

# v1
router_v1 = APIRouter(prefix="/api/v1")

@router_v1.get("/tareas")
def listar_tareas_v1():
    # Version 1: Lista simple
    return {"tareas": []}

# v2
router_v2 = APIRouter(prefix="/api/v2")

@router_v2.get("/tareas")
def listar_tareas_v2(
    pagina: int = 1,
    por_pagina: int = 10
):
    # Version 2: Con paginacion
    return {
        "items": [],
        "total": 0,
        "pagina": pagina,
        "por_pagina": por_pagina
    }

# Registrar en app
app.include_router(router_v1)
app.include_router(router_v2)

# Otra opcion: Header versioning
@app.middleware("http")
async def version_middleware(request: Request, call_next):
    version = request.headers.get("X-API-Version", "v1")
    request.state.api_version = version
    return await call_next(request)
Mejores practicas:
  • Mantener retrocompatibilidad
  • Documentar cambios entre versiones
  • Deprecar versiones antiguas gradualmente

Ejercicio: API Completa (15 min)

Implementar endpoint completo para Tareas:

  • GET /tareas - Listar con paginacion y filtros
  • GET /tareas/{id} - Obtener por ID
  • POST /tareas - Crear
  • PATCH /tareas/{id} - Actualizar
  • DELETE /tareas/{id} - Eliminar
E6 Proyecto Final: La API debe implementar todas las practicas vistas.

Resumen

  • Paginacion: Limitar resultados con offset/limit
  • Filtros: Parametros Query para filtrar
  • Errores: HTTPException y handlers globales
  • Versionado: URL path o header versioning
  • Documentacion: OpenAPI automatico de FastAPI