Module 5: Error Handling and CORS

Introducción al Módulo 5: Error Handling y CORS

Descripción

Tu API tiene CRUD completo, parámetros validados con Query() y Path(), modelos Pydantic que definen contratos claros para requests y responses. Es una API funcional. Pero hay un problema que no has resuelto: ¿qué pasa cuando las cosas salen mal? Cuando un cliente pide un libro que no existe, ¿qué recibe? Cuando envía un ID duplicado, ¿qué le dice la API? Cuando un frontend en localhost:3000 intenta consumir tu API en localhost:8000, ¿por qué el navegador bloquea la petición? Estas preguntas no se responden con más endpoints ni con más modelos Pydantic. Se responden con error handling profesional y configuración de CORS.

Sin error handling estructurado, tu API es impredecible. Algunos endpoints retornan {"detail": "Not found"}, otros retornan {"error": "libro no encontrado"}, otros simplemente explotan con un 500 Internal Server Error que no le dice nada útil al cliente. El frontend no puede parsear errores de forma consistente porque cada endpoint inventa su propio formato. Y sin CORS, tu API es invisible para cualquier frontend que corra en un dominio diferente — que en desarrollo es prácticamente siempre. Un React app en localhost:3000 no puede hacer fetch a localhost:8000 sin que el navegador lo bloquee. No es un bug de tu código, es una política de seguridad del navegador que necesitas manejar explícitamente.

Este módulo cierra las dos brechas de robustez más importantes de tu API. Después de completarlo, tus errores serán consistentes, informativos y parseables por cualquier cliente. Y tu API será consumible desde cualquier frontend, mobile app o servicio externo que necesite acceder a ella. Es el paso que separa una API que funciona en /docs de una API que funciona en el mundo real.


¿Dónde estamos en la guía?

Estás en el Módulo 5 de 6 de la guía FastAPI Fundamentals:

Módulo 1: Setup y Primera API ✅ (completado)
    → Instalación, uvicorn, endpoints GET, documentación automática

Módulo 2: Path Operations ✅ (completado)
    → GET, POST, PUT, PATCH, DELETE — CRUD completo

Módulo 3: Request y Response ✅ (completado)
    → Query params, Path(), Query(), Body(), filtros, paginación

Módulo 4: Pydantic y Validación ✅ (completado)
    → BaseModel, Field(), validators, modelos separados, response_model

Módulo 5: Error Handling y CORS ← ESTÁS AQUÍ
    → HTTPException, custom handlers, CORS middleware, status codes

Módulo 6: Proyecto Final — To-Do List API

Progresión acumulativa

Cada módulo agrega una capa sobre el anterior:

Módulo 1: Servidor corriendo + endpoints GET básicos
    ↓ (base)
Módulo 2: + CRUD completo (POST, PUT, PATCH, DELETE)
    ↓ (operaciones)
Módulo 3: + Parámetros avanzados, filtros, paginación
    ↓ (datos de entrada)
Módulo 4: + Modelos Pydantic, validación estructurada, contratos
    ↓ (datos estructurados)
Módulo 5: + Error handling profesional + CORS  ← AQUÍ
    ↓ (robustez)
Módulo 6: To-Do List API (integración total)

El Módulo 4 te dio contratos para los datos: qué acepta tu API y qué retorna cuando todo sale bien. El Módulo 5 define qué pasa cuando las cosas salen mal — y cómo tu API se abre al mundo exterior. Estás casi en la meta: después de este módulo solo queda el proyecto final que integra todo.


Lo que ya dominas vs Lo nuevo

Lo que ya dominas

De los Módulos 1-4 traes:

  • ✅ Virtual environment con FastAPI, uvicorn y Pydantic v2 funcionando
  • ✅ Estructura de proyecto app/main.py con Books API
  • ✅ Endpoints CRUD completos con modelos Pydantic (BookCreate, BookUpdate, BookResponse)
  • ✅ Validación automática con Pydantic — errores 422 para datos inválidos
  • JSONResponse para retornar status codes custom (usado en algunos endpoints)
  • ✅ Lógica básica de "not found" con if/else en endpoints GET y DELETE
  • response_model para controlar datos de respuesta
  • Field(), @field_validator, model_dump() con opciones

Lo nuevo de este módulo

  • 🆕 HTTPException — la forma estándar de comunicar errores en FastAPI
  • 🆕 Status codes semánticos — 400, 404, 409, 403, 422 usados con intención
  • 🆕 Custom exception handlers — capturar excepciones y formatear la respuesta
  • 🆕 Formato de error consistente — todos los errores siguen la misma estructura
  • 🆕 CORSMiddleware — permitir que otros dominios consuman tu API
  • 🆕 Middleware como concepto — Request → Middleware → Handler → Middleware → Response
  • 🆕 Logging básico — registrar errores para debugging
  • 🆕 Archivos estáticos — servir archivos desde tu API

De errores ad-hoc a errores profesionales

AspectoTu API actual (Módulo 4)Tu API con error handling (Módulo 5)
Recurso no encontradoif not book: return {"error": "not found"} con status 200raise HTTPException(status_code=404, detail="Book not found")
Formato de erroresCada endpoint inventa su formatoTodos los errores siguen {"detail": "...", "status_code": N}
ID duplicadoNo se validaHTTPException(409, "Book with this ISBN already exists")
Status codesAlgunos 200, algunos 404, inconsistenteCada error usa el código HTTP semánticamente correcto
Errores inesperados500 genérico sin contextoCustom handler que loguea el error y retorna respuesta útil
Frontend externoBloqueado por el navegadorCORSMiddleware permite dominios específicos

El problema: errores inconsistentes

Para entender por qué este módulo importa, veamos cómo tu API maneja errores ahora mismo. Después de cuatro módulos, tienes endpoints que manejan el caso "no encontrado" de diferentes maneras:

# Endpoint 1 — retorna dict con status 200 (incorrecto)
@app.get("/books/{book_id}")
def get_book(book_id: int):
    book = find_book(book_id)
    if not book:
        return {"error": "Book not found"}  # status 200 — el cliente cree que fue exitoso
    return book

# Endpoint 2 — usa JSONResponse (mejor, pero formato propio)
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    book = find_book(book_id)
    if not book:
        return JSONResponse(
            status_code=404,
            content={"message": "No se encontró el libro"}
        )
    books.remove(book)
    return {"status": "deleted"}

# Endpoint 3 — no maneja el error (peor caso)
@app.put("/books/{book_id}")
def update_book(book_id: int, book_data: BookUpdate):
    book = find_book(book_id)
    book["title"] = book_data.title  # si book es None → 500 Internal Server Error
    return book

Tres endpoints, tres formas diferentes de manejar el mismo error. Un cliente que consume tu API tiene que adivinar qué formato de error esperar en cada caso. ¿El campo se llama "error", "message" o "detail"? ¿El status code es 200, 404 o 500? ¿La respuesta tiene siempre la misma estructura? No hay forma de saberlo sin probar cada endpoint individualmente.

Ahora imagina que eres el desarrollador frontend. Necesitas escribir código que maneje errores de tu API:

// ¿Cómo parseo el error? No sé qué esperar
try {
    const response = await fetch("/api/books/999");
    if (!response.ok) {
        const error = await response.json();
        // ¿Es error.error? ¿error.message? ¿error.detail?
        // Depende del endpoint... 😬
    }
} catch (e) {
    // ¿500 sin body? ¿Timeout? ¿CORS blocked?
}

Esto no es un problema teórico. Es la razón por la que los equipos frontend y backend tienen reuniones interminables sobre "el formato de los errores." Un formato consistente elimina esa conversación.


Dos problemas, un módulo

Este módulo agrupa dos temas que podrían parecer no relacionados. Pero ambos responden a la misma pregunta: ¿está tu API lista para que alguien la consuma de verdad?

Problema 1: Error Handling — ¿Cómo comunica la API qué salió mal?

Cuando una operación falla, el cliente necesita tres cosas:

  1. Un status code HTTP correcto — 404 para "no existe", 400 para "datos inválidos", 409 para "conflicto"
  2. Un formato de error predecible — siempre la misma estructura JSON
  3. Un mensaje útil — que explique qué falló y, idealmente, qué hacer al respecto

Sin estas tres cosas, el cliente solo sabe que "algo falló." Con ellas, el cliente puede mostrar un mensaje de error específico, reintentar la operación, o redirigir al usuario — automáticamente, sin intervención humana.

Problema 2: CORS — ¿Cómo permite la API que otros dominios la consuman?

Tu API funciona perfectamente en /docs. Funciona en Postman. Funciona con curl. Pero cuando un frontend React en http://localhost:3000 hace fetch("http://localhost:8000/books"), el navegador bloquea la petición. No es un bug — es la Same-Origin Policy, una política de seguridad que impide que un sitio web haga requests a otro dominio sin permiso explícito. CORS (Cross-Origin Resource Sharing) es el mecanismo que le dice al navegador: "este dominio tiene permiso para acceder a mi API."

¿Por qué en el mismo módulo?

Ambos son requisitos no negociables para producción. Una API sin error handling consistente genera frustración en los consumidores. Una API sin CORS es literalmente inaccesible desde un navegador. Además, CORS se implementa como middleware en FastAPI, y el concepto de middleware es útil para entender cómo se procesan los requests en general — incluyendo cómo se interceptan errores.


Status Codes HTTP — El lenguaje universal

Antes de escribir una línea de error handling, necesitas entender el sistema de comunicación que HTTP te da: los status codes. Son el primer canal de información entre tu API y sus clientes. Un cliente bien escrito revisa el status code antes de leer el body de la respuesta.

Los status codes que vas a usar

2xx — Éxito (todo salió bien)

CódigoNombreCuándo usarlo
200OKRequest procesado exitosamente. GET que retorna datos, PUT/PATCH que actualizan
201CreatedSe creó un recurso nuevo. POST exitoso
204No ContentOperación exitosa sin datos que retornar. DELETE exitoso

4xx — Error del cliente (el cliente hizo algo mal)

CódigoNombreCuándo usarlo
400Bad RequestDatos inválidos que no cubren los otros 4xx. Request malformado
401UnauthorizedNo autenticado. Falta token o credenciales
403ForbiddenAutenticado pero sin permisos. Sabe quién eres, no puedes hacer esto
404Not FoundEl recurso no existe. /books/999 cuando no hay libro 999
409ConflictConflicto con el estado actual. Crear un libro con ISBN duplicado
422Unprocessable EntityDatos con formato correcto pero semánticamente inválidos. Pydantic los genera

5xx — Error del servidor (el servidor hizo algo mal)

CódigoNombreCuándo usarlo
500Internal Server ErrorError inesperado en el servidor. Bug, excepción no manejada

¿Por qué importan los status codes?

Los status codes no son decorativos. Son el mecanismo principal con el que tu API comunica el resultado de una operación. Un cliente HTTP (navegador, frontend, mobile app, otro servicio) toma decisiones basándose en el código:

  • 200-299: La operación fue exitosa. Muestra los datos al usuario
  • 400-499: El cliente cometió un error. Muestra un mensaje de error, pide corrección
  • 500-599: El servidor falló. Reintenta la operación, muestra "intenta más tarde"

Cuando tu endpoint retorna {"error": "not found"} con status 200, el cliente lo interpreta como éxito. Tiene que parsear el body para descubrir que en realidad falló. Eso rompe el contrato HTTP y hace que el error handling del lado del cliente sea innecesariamente complejo.

401 y 403 en este módulo

Vas a aprender qué significan 401 (Unauthorized) y 403 (Forbidden) porque son parte del vocabulario de status codes que todo desarrollador backend necesita conocer. Pero no vas a implementar autenticación en este módulo — eso viene en el path de Backend Python Developer. Aquí los mencionamos para que tu modelo mental de status codes esté completo.


Errores son parte del contrato

En el Módulo 4 aprendiste que los modelos Pydantic definen el contrato de éxito: qué acepta tu API y qué retorna cuando todo sale bien. Pero un contrato completo incluye también el contrato de fallo: qué retorna tu API cuando algo sale mal.

El contrato de éxito (ya lo tienes)

POST /books con BookCreate válido → 201 + BookResponse
GET /books/5 → 200 + BookResponse

El contrato de fallo (lo que vas a construir)

POST /books con datos inválidos → 422 + ErrorResponse (Pydantic automático)
GET /books/999 → 404 + ErrorResponse
POST /books con ISBN duplicado → 409 + ErrorResponse
PUT /books/999 con body vacío → 400 + ErrorResponse

¿Qué necesita saber el cliente?

Para cada posible error, el cliente necesita tres piezas de información:

  1. Status code — el número que indica la categoría del error
  2. Formato — la estructura JSON del error (qué campos tiene)
  3. Contenido — el mensaje que describe qué falló

Cuando estos tres elementos son consistentes a través de toda tu API, el cliente puede escribir un solo handler de errores que funciona para todos los endpoints:

Si status >= 400:
    error = response.json()
    mostrar error["detail"]

Ese handler funciona porque todos tus endpoints de error retornan la misma estructura. Sin consistencia, el cliente necesita un handler diferente por endpoint — o peor, un try/catch genérico que muestra "Ocurrió un error" sin contexto.

FastAPI y HTTPException: el estándar

FastAPI proporciona HTTPException como la forma estándar de comunicar errores. Cuando haces raise HTTPException(status_code=404, detail="Book not found"), FastAPI automáticamente:

  1. Detiene la ejecución del endpoint
  2. Retorna una respuesta con status code 404
  3. Incluye el body {"detail": "Book not found"}

El campo detail es la convención de FastAPI. Todos los errores de Pydantic ya usan ese campo. Cuando tú también lo usas, el cliente tiene un formato unificado para todos los errores — ya sean de validación automática o de lógica de negocio.


¿Qué es CORS? (Conceptual)

Antes de configurar CORS en FastAPI, necesitas entender por qué existe. No es una decisión arbitraria de los navegadores — es una protección de seguridad fundamental que resuelve un problema real.

Same-Origin Policy

Los navegadores implementan una regla llamada Same-Origin Policy: un script en una página web solo puede hacer requests HTTP al mismo origen de donde se cargó la página. Un "origen" se define por tres componentes: protocolo + dominio + puerto.

http://localhost:3000  ← Origen del frontend React
http://localhost:8000  ← Origen de tu API FastAPI

¿Son el mismo origen? NO — el puerto es diferente

Cuando tu frontend en localhost:3000 intenta hacer fetch("http://localhost:8000/books"), el navegador ve que los orígenes son diferentes y bloquea la petición. No llega a tu API. El navegador la detiene antes.

¿Por qué existe esta restricción?

Imagina que no existiera. Entras a un sitio malicioso evil-site.com. Ese sitio tiene un script que hace fetch("https://tu-banco.com/api/transferir?a=hacker&monto=10000"). Si estás logueado en tu banco, el navegador enviaría tus cookies de sesión con el request. Sin Same-Origin Policy, el script de evil-site.com podría operar tu cuenta bancaria. Same-Origin Policy existe para que un sitio web no pueda hacer requests a otro sitio en tu nombre.

CORS: excepciones controladas

CORS (Cross-Origin Resource Sharing) no desactiva la Same-Origin Policy. La extiende con un mecanismo de permisos. Tu API puede declarar explícitamente: "acepto requests desde estos orígenes específicos." El navegador verifica esa declaración antes de permitir el request.

El flujo simplificado:

1. Frontend (localhost:3000) quiere hacer GET a API (localhost:8000)
2. Navegador envía un "preflight request" (OPTIONS) a la API
3. API responde con headers: "Permito requests desde localhost:3000"
4. Navegador verifica → el origen está permitido → deja pasar el GET
5. Si la API no responde con los headers correctos → navegador bloquea

¿Quién necesita CORS?

  • Frontends web (React, Vue, Angular, vanilla JS) — siempre necesitan CORS si están en un dominio diferente
  • Postman, curl, HTTPie — no son navegadores, no aplica Same-Origin Policy
  • Otro backend (server-to-server) — no son navegadores, no aplica
  • Swagger UI (/docs) — se sirve desde el mismo origen que tu API, no necesita CORS

Esto explica por qué tu API funciona perfectamente en /docs y en Postman pero falla cuando un frontend React intenta consumirla. El problema no es tu API — es el navegador aplicando Same-Origin Policy.

CORS en desarrollo vs producción

En desarrollo es común permitir todos los orígenes (origins=["*"]) para no complicarse. En producción, debes listar explícitamente los dominios permitidos (origins=["https://mi-frontend.com"]). La diferencia tiene implicaciones de seguridad que verás en las cápsulas técnicas.


Middleware — el concepto

CORS se implementa en FastAPI como un middleware. Antes de ver cómo, necesitas entender qué es middleware y por qué existe.

¿Qué es middleware?

Middleware es código que se ejecuta antes y después de cada request, pero no pertenece a ningún endpoint específico. Es una capa que envuelve toda tu aplicación:

Request del cliente
    ↓
┌─────────────────────┐
│  Middleware (antes)  │  ← Procesa el request antes de llegar al endpoint
└─────────────────────┘
    ↓
┌─────────────────────┐
│  Tu endpoint/handler │  ← Tu función de FastAPI
└─────────────────────┘
    ↓
┌─────────────────────┐
│  Middleware (después) │  ← Procesa la response antes de enviarla al cliente
└─────────────────────┘
    ↓
Response al cliente

¿Para qué se usa middleware?

Middleware es ideal para lógica que se aplica a todos los requests, no a endpoints individuales:

  • 🔄 CORS — agregar headers de permisos a cada response
  • ⏱️ Timing — medir cuánto tarda cada request
  • 📝 Logging — registrar cada request que llega
  • 🔒 Autenticación — verificar tokens antes de llegar al endpoint
  • 🗜️ Compresión — comprimir responses automáticamente

¿Por qué CORS es middleware?

CORS necesita agregar headers específicos a cada response de tu API. No a un endpoint, no a una ruta — a cada response sin excepción. Con middleware, lo configuras una vez y aplica a todo automáticamente.

Múltiples middlewares

Puedes tener varios middlewares encadenados. Se ejecutan en orden: Request → CORS → Logging → Endpoint → Logging → CORS → Response. Cada uno puede modificar el request al entrar y la response al salir. El orden importa, pero eso lo verás en las cápsulas técnicas.


Objetivo del módulo

Al completar este módulo serás capaz de:

  • ✅ Usar HTTPException para comunicar errores con status codes semánticos
  • ✅ Aplicar el status code correcto para cada situación: 400, 404, 409, 422
  • ✅ Crear custom exception handlers que formatean errores de manera consistente
  • ✅ Definir un formato de error estándar para toda tu API
  • ✅ Configurar CORSMiddleware para permitir acceso desde frontends específicos
  • ✅ Entender Same-Origin Policy y por qué CORS existe
  • ✅ Implementar middleware básico en FastAPI
  • ✅ Agregar logging para registrar errores y requests
  • ✅ Servir archivos estáticos desde tu API
  • ✅ Distinguir cuándo usar 400 vs 404 vs 409 vs 422
  • ✅ Configurar CORS de forma segura para desarrollo y producción

Prerequisitos

Para este módulo necesitas:

  • Módulos 1-4 completados — Books API con modelos Pydantic, validación, response_model
  • Tu proyecto Books API — Con modelos BookCreate, BookUpdate, BookResponse funcionando
  • uvicorn corriendo con --reload — Para probar cambios en tiempo real

Verificación rápida

cd fastapi-fundamentals
source venv/bin/activate
uvicorn app.main:app --reload

Abre http://localhost:8000/docs y verifica que puedes:

  1. Crear un libro con POST /books usando un body que sigue el schema BookCreate
  2. Obtener un libro por ID con GET /books/{book_id} — incluyendo un ID que no existe
  3. Actualizar un libro con PATCH /books/{book_id} con BookUpdate
  4. Ver schemas detallados en /docs con tipos, constraints y ejemplos

Si eso funciona, estás listo para el Módulo 5.

¿Qué pasa si no cumples algún prerequisito?

Prerequisito faltanteQué hacer
No hiciste el Módulo 4Completa las cápsulas 02-05 del Módulo 4 primero — necesitas modelos Pydantic
Tu Books API no tiene modelos PydanticVuelve a la Cápsula 05 del Módulo 4 e implementa la migración de dicts a modelos
No usas response_modelRevisa la Cápsula 04 del Módulo 4 — lo necesitarás para controlar respuestas de error
Tu Books API no levantaRevisa errores de importación y verifica que FastAPI y Pydantic v2 están instalados

Roadmap del módulo

CápsulaTemaQué aprenderás
01Introducción (esta cápsula)Contexto, el problema de errores inconsistentes, CORS conceptual, roadmap
02HTTPException y status codesraise HTTPException, status codes semánticos, errores en CRUD
03Custom exception handlersCapturar excepciones, formato consistente, logging de errores
04CORS y MiddlewareCORSMiddleware, Same-Origin Policy, configuración dev/prod, archivos estáticos
05Proyecto: Books API robustaError handling completo + CORS en tu Books API, tests manuales

Flujo de aprendizaje

Primero aprenderás HTTPException y cómo usarlo con status codes semánticos para comunicar errores claros en todos tus endpoints CRUD (Cápsula 02). Después crearás custom exception handlers que capturan excepciones de forma centralizada y retornan errores en un formato consistente — además de agregar logging para debugging (Cápsula 03). Luego configurarás CORSMiddleware para que tu API sea accesible desde frontends externos, entenderás el concepto de middleware, y agregarás servicio de archivos estáticos (Cápsula 04). Al final, integrarás todo en tu Books API: error handling completo en cada endpoint, CORS configurado, y logging funcionando (Cápsula 05).

La progresión es: comunicar errores → centralizar errores → abrir al mundo → integrar todo.

Detalle por cápsula

Cápsula 02 — HTTPException y status codes: Reemplazarás los if/else con return {"error": "..."} por raise HTTPException(status_code=404, detail="Book not found"). Aprenderás cuándo usar cada status code: 404 para recursos que no existen, 400 para requests malformados, 409 para conflictos (como ISBN duplicado). Verás cómo raise detiene la ejecución del endpoint inmediatamente — no necesitas else. Y cómo el módulo status de starlette te da constantes legibles como status.HTTP_404_NOT_FOUND.

Cápsula 03 — Custom exception handlers: Cuando HTTPException no es suficiente, creas handlers personalizados. Registrarás un handler que captura todas las HTTPException y las formatea con una estructura consistente. Crearás un handler para RequestValidationError que mejora los mensajes de error 422 de Pydantic. Agregarás un handler catch-all para excepciones inesperadas que loguea el traceback y retorna un 500 limpio. Aprenderás logging de Python para registrar errores con nivel, timestamp y contexto.

Cápsula 04 — CORS y Middleware: Configurarás CORSMiddleware paso a paso: orígenes permitidos, métodos permitidos, headers permitidos. Verás la diferencia entre allow_origins=["*"] (desarrollo) y allow_origins=["https://mi-app.com"] (producción). Entenderás preflight requests (OPTIONS) y qué headers CORS agrega el middleware. Aprenderás a crear tu propio middleware básico. Y configurarás StaticFiles para servir archivos estáticos desde tu API.

Cápsula 05 — Proyecto Books API robusta: La integración completa. Cada endpoint de tu Books API tendrá error handling apropiado con el status code correcto. Los errores seguirán un formato unificado. CORS estará configurado para desarrollo. Logging registrará operaciones y errores. Y probarás manualmente cada caso de error para verificar que la respuesta es correcta y consistente.


¿Qué NO se cubre en este módulo?

Es importante saber los límites para no buscar algo que viene después o que está fuera del scope:

  • Autenticación / Autorización — Los status codes 401 y 403 se explican conceptualmente, pero no implementas login, tokens JWT ni verificación de permisos. Eso se cubre en el path Backend Python Developer
  • Rate limiting — Limitar requests por IP o usuario es importante en producción pero requiere almacenamiento (Redis) que está fuera del scope de fundamentals
  • Middleware avanzado — Middleware con estado, middleware asíncrono complejo, o middleware que modifica el body del request. Aquí cubres el concepto y CORSMiddleware
  • Retry logic — Reintentar operaciones fallidas es responsabilidad del cliente, no del servidor
  • Circuit breaker pattern — Patrones de resiliencia para servicios distribuidos están fuera del scope
  • Structured logging (JSON) — Logging avanzado con formatos JSON para sistemas de monitoreo se cubre en guías de producción

¿Por qué estos límites?

  • Autenticación es otro dominio. Error handling te dice cómo comunicar errores. Autenticación te dice quién puede hacer qué. Son ortogonales. Implementar auth aquí mezclaría dos conceptos que se entienden mejor por separado
  • Rate limiting necesita estado. Para contar requests por IP necesitas almacenamiento persistente (Redis, base de datos). Tu API todavía trabaja en memoria
  • Middleware avanzado es raro en APIs simples. El 90% de las APIs solo necesitan CORS middleware. Los otros patrones son para servicios complejos en producción
  • Logging JSON es para infraestructura. logging.error() es suficiente para desarrollo. Structured logging importa cuando tienes Datadog, CloudWatch o ELK Stack

Cada límite es intencional. Error handling y CORS son lo que necesitas para que tu API sea consumible por clientes reales. El resto viene cuando tengas las bases sólidas.


Conexión con el proyecto

Tu Books API recibe su último upgrade antes del proyecto final. Así se ve el cambio:

Antes (Módulo 4) — errores inconsistentes, sin CORS

@app.get("/books/{book_id}", response_model=BookResponse)
def get_book(book_id: int):
    book = find_book(book_id)
    if not book:
        return JSONResponse(status_code=404, content={"error": "not found"})
    return book

@app.delete("/books/{book_id}")
def delete_book(book_id: int):
    book = find_book(book_id)
    if not book:
        return {"message": "Book not found"}  # status 200!
    books.remove(book)
    return {"status": "deleted"}
  • Cada endpoint tiene su formato de error
  • Algunos retornan 200 cuando deberían retornar 404
  • No hay CORS — un frontend no puede consumir la API
  • No hay logging — si algo falla, no hay registro

Después (Módulo 5) — errores profesionales, CORS configurado

@app.get("/books/{book_id}", response_model=BookResponse)
def get_book(book_id: int):
    book = find_book(book_id)
    if not book:
        raise HTTPException(status_code=404, detail="Book not found")
    return book

@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int):
    book = find_book(book_id)
    if not book:
        raise HTTPException(status_code=404, detail="Book not found")
    books.remove(book)
  • Todos los errores usan HTTPException con el status code correcto
  • El formato es siempre {"detail": "..."} — consistente y predecible
  • CORS middleware permite acceso desde frontends configurados
  • Logging registra errores con contexto para debugging

La Books API como proyecto evolutivo

Módulo 1: Hello World API (esqueleto vacío)
    ↓
Módulo 2: Books API con CRUD (operaciones básicas con dicts)
    ↓
Módulo 3: Books API con filtros y validación de params
    ↓
Módulo 4: Books API con Pydantic (dicts → modelos)
    ↓
Módulo 5: Books API con error handling y CORS  ← AQUÍ
    ↓
Módulo 6: To-Do List API (proyecto nuevo integrando todo)

La transición de este módulo es menos visual que la del Módulo 4 (donde los schemas en /docs cambiaron dramáticamente), pero es igual de importante. Después de este módulo, tu API no solo funciona — funciona correctamente cuando las cosas salen mal. Y eso es lo que separa una API de desarrollo de una API lista para el mundo real.


Evidencia de éxito

Al terminar este módulo (las 5 cápsulas), sabrás que tuviste éxito si:

  • ✅ Todos tus endpoints usan HTTPException en lugar de return {"error": "..."} o JSONResponse manual
  • ✅ Cada error retorna el status code HTTP semánticamente correcto (404 para no encontrado, 409 para conflicto, 400 para request inválido)
  • ✅ Tienes al menos un custom exception handler registrado con @app.exception_handler
  • ✅ Todos los errores siguen el mismo formato JSON — el cliente puede parsearlos con un solo handler
  • CORSMiddleware está configurado con orígenes, métodos y headers explícitos
  • ✅ Puedes explicar qué es Same-Origin Policy y por qué CORS existe
  • ✅ Logging básico registra errores con timestamp y contexto
  • ✅ Tu API sirve archivos estáticos con StaticFiles
  • ✅ Enviar un request a un recurso inexistente retorna 404, no 200 ni 500
  • ✅ Un frontend en otro puerto puede consumir tu API sin errores de CORS
  • ✅ Entiendes la diferencia entre raise HTTPException(...) y return JSONResponse(...)

Resumen

  • Este es el Módulo 5 de 6, enfocado en error handling profesional y configuración de CORS
  • Tu API tiene CRUD y validación Pydantic, pero los errores son inconsistentes y el CORS no está configurado
  • HTTPException es la forma estándar de comunicar errores en FastAPI — reemplaza return {"error": "..."}
  • Los status codes HTTP son el lenguaje universal: 200 éxito, 404 no encontrado, 409 conflicto, 500 error del servidor
  • Errores son parte del contrato de tu API — así como defines el formato de éxito, defines el formato de fallo
  • Same-Origin Policy es una protección del navegador. CORS es el mecanismo para crear excepciones controladas
  • Middleware es código que se ejecuta antes y después de cada request — CORS se implementa como middleware
  • Custom exception handlers permiten centralizar el formato de errores y agregar logging
  • Tu Books API recibe error handling consistente y CORS — lista para que un frontend la consuma
  • No se cubre: autenticación (401/403 implementado), rate limiting, middleware avanzado, structured logging
  • La progresión es: comunicar errores → centralizar errores → abrir al mundo → integrar todo

Recursos adicionales

  1. FastAPI - Handling Errors — Tutorial oficial de HTTPException y custom exception handlers
  2. FastAPI - HTTPException — Referencia de la clase HTTPException y sus parámetros
  3. FastAPI - CORS — Tutorial oficial de configuración de CORSMiddleware
  4. MDN - Cross-Origin Resource Sharing (CORS) — Guía completa de CORS desde la perspectiva del navegador
  5. MDN - HTTP Status Codes — Referencia completa de todos los status codes HTTP
  6. FastAPI - Middleware — Cómo crear y usar middleware en FastAPI

Siguiente cápsula: HTTPException y status codes — Reemplazarás los errores ad-hoc de tu API por raise HTTPException(...) con status codes semánticos, y verás cómo una línea de código comunica más que diez líneas de if/else con return.