FastAPI
Tutorial FastAPI: Respuestas y códigos de estado
Aprende a manejar códigos de estado HTTP en FastAPI para crear APIs REST robustas y claras con respuestas precisas y personalizadas.
Aprende FastAPI y certifícateCódigos HTTP y su significado en APIs REST
Los códigos de estado HTTP son una parte fundamental de cualquier API REST, ya que proporcionan información inmediata sobre el resultado de una solicitud. Estos códigos numéricos de tres dígitos permiten a los clientes entender rápidamente si su petición fue exitosa, si requiere alguna acción adicional o si ha ocurrido algún error.
Estructura de los códigos HTTP
Los códigos de estado HTTP se agrupan en cinco categorías principales, identificables por el primer dígito:
- 1xx (Informativo): La solicitud fue recibida y el proceso continúa.
- 2xx (Éxito): La solicitud fue recibida, entendida y aceptada correctamente.
- 3xx (Redirección): Se requieren acciones adicionales para completar la solicitud.
- 4xx (Error del cliente): La solicitud contiene errores o no puede ser procesada.
- 5xx (Error del servidor): El servidor falló al procesar una solicitud aparentemente válida.
Implementación en FastAPI
FastAPI facilita enormemente el manejo de códigos de estado HTTP a través del parámetro status_code
en los decoradores de ruta. Veamos cómo implementarlo:
from fastapi import FastAPI, status
app = FastAPI()
@app.get("/items/", status_code=status.HTTP_200_OK)
def read_items():
return {"items": ["Item 1", "Item 2"]}
En este ejemplo, estamos especificando explícitamente que la respuesta tendrá un código de estado 200 (OK). FastAPI proporciona constantes a través del módulo status
que hacen el código más legible y menos propenso a errores.
Códigos de éxito (2xx)
Los códigos 2xx indican que la solicitud del cliente fue recibida, entendida y procesada correctamente.
- 200 OK: La solicitud se completó con éxito. Es el código por defecto para respuestas exitosas.
@app.get("/users/{user_id}")
def read_user(user_id: int):
# Lógica para obtener usuario
return {"user_id": user_id, "name": "Usuario Ejemplo"}
- 201 Created: El recurso se creó correctamente. Ideal para respuestas a solicitudes POST.
@app.post("/users/", status_code=status.HTTP_201_CREATED)
def create_user(name: str):
# Lógica para crear usuario
return {"id": 1, "name": name}
- 204 No Content: La solicitud se completó con éxito, pero no hay contenido para devolver.
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(user_id: int):
# Lógica para eliminar usuario
return None
Códigos de redirección (3xx)
Los códigos 3xx indican que el cliente debe tomar alguna acción adicional para completar la solicitud.
- 301 Moved Permanently: El recurso se ha movido permanentemente a otra URL.
- 307 Temporary Redirect: Redirección temporal a otra URL.
En FastAPI, puedes implementar redirecciones así:
from fastapi import FastAPI, status
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/old-location")
def redirect_to_new():
return RedirectResponse(
url="/new-location",
status_code=status.HTTP_301_MOVED_PERMANENTLY
)
Códigos de error del cliente (4xx)
Los códigos 4xx indican que hubo un error en la solicitud del cliente.
- 400 Bad Request: La solicitud no pudo ser entendida por el servidor debido a una sintaxis incorrecta.
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
if item_id <= 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="El ID del ítem debe ser positivo"
)
return {"item_id": item_id}
- 401 Unauthorized: Es necesaria autenticación para acceder al recurso.
- 403 Forbidden: El servidor entendió la solicitud, pero se niega a autorizarla.
- 404 Not Found: El recurso solicitado no existe en el servidor.
@app.get("/users/{user_id}")
def get_user(user_id: int):
# Simulamos búsqueda en base de datos
user = None # Supongamos que no se encontró
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Usuario con id {user_id} no encontrado"
)
return {"user_id": user_id, "name": "Usuario Ejemplo"}
- 422 Unprocessable Entity: La solicitud está bien formada pero no puede ser procesada debido a errores semánticos.
FastAPI utiliza este código automáticamente cuando falla la validación de datos con Pydantic.
Códigos de error del servidor (5xx)
Los códigos 5xx indican que el servidor falló al procesar una solicitud aparentemente válida.
- 500 Internal Server Error: Error genérico cuando ocurre una condición inesperada.
@app.get("/process")
def process_data():
try:
# Operación que podría fallar
result = complex_operation()
return {"result": result}
except Exception:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Ocurrió un error al procesar los datos"
)
- 503 Service Unavailable: El servidor no está disponible temporalmente.
Uso de HTTPException en FastAPI
FastAPI proporciona la clase HTTPException
para lanzar excepciones con códigos de estado HTTP específicos:
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
items = {1: "Laptop", 2: "Smartphone"}
if item_id not in items:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item con id {item_id} no encontrado",
headers={"X-Error": "Item no existe"}, # Headers opcionales
)
return {"item_id": item_id, "name": items[item_id]}
La excepción HTTPException
acepta tres parámetros principales:
status_code
: El código de estado HTTP a devolver.detail
: Información detallada sobre el error.headers
: Headers HTTP adicionales (opcional).
Mejores prácticas para códigos de estado
Al diseñar una API REST con FastAPI, considera estas recomendaciones:
- Usa códigos específicos en lugar de genéricos para proporcionar información más precisa.
- Mantén la consistencia en toda tu API (usa los mismos códigos para situaciones similares).
- Proporciona mensajes de error descriptivos en el campo
detail
de las excepciones. - Utiliza las constantes del módulo
status
en lugar de números directamente. - Para operaciones CRUD, sigue estas convenciones:
- GET: 200 OK para éxito, 404 Not Found si el recurso no existe
- POST: 201 Created para creación exitosa
- PUT/PATCH: 200 OK o 204 No Content para actualizaciones exitosas
- DELETE: 204 No Content para eliminaciones exitosas
Ejemplo completo con diferentes códigos de estado
Veamos un ejemplo más completo que implementa varios códigos de estado en una API de gestión de tareas:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
from typing import Dict, List, Optional
app = FastAPI()
# Modelo de datos
class Task(BaseModel):
title: str
description: Optional[str] = None
completed: bool = False
# Almacenamiento en memoria
tasks_db: Dict[int, Task] = {}
task_counter = 0
# Crear tarea
@app.post("/tasks/", status_code=status.HTTP_201_CREATED)
def create_task(task: Task):
global task_counter
task_counter += 1
tasks_db[task_counter] = task
return {"id": task_counter, **task.model_dump()}
# Obtener todas las tareas
@app.get("/tasks/", response_model=List[dict])
def read_tasks():
return [{"id": id, **task.model_dump()} for id, task in tasks_db.items()]
# Obtener una tarea específica
@app.get("/tasks/{task_id}")
def read_task(task_id: int):
if task_id not in tasks_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tarea con id {task_id} no encontrada"
)
return {"id": task_id, **tasks_db[task_id].model_dump()}
# Actualizar una tarea
@app.put("/tasks/{task_id}")
def update_task(task_id: int, task: Task):
if task_id not in tasks_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tarea con id {task_id} no encontrada"
)
tasks_db[task_id] = task
return {"id": task_id, **task.model_dump()}
# Eliminar una tarea
@app.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_task(task_id: int):
if task_id not in tasks_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tarea con id {task_id} no encontrada"
)
del tasks_db[task_id]
return None
Este ejemplo muestra cómo implementar diferentes códigos de estado para las operaciones CRUD típicas en una API REST. Cada endpoint utiliza el código de estado apropiado según la operación y maneja posibles errores con excepciones HTTP adecuadas.
Response Models y personalización
Cuando desarrollamos APIs con FastAPI, no solo es importante devolver los códigos de estado adecuados, sino también estructurar correctamente los datos de respuesta. FastAPI ofrece herramientas potentes para definir y personalizar exactamente cómo se verán los datos que nuestra API devuelve a los clientes.
Definiendo modelos de respuesta
Los Response Models en FastAPI permiten declarar el tipo de datos que una ruta devolverá. Esto proporciona varias ventajas:
- Validación automática de la respuesta
- Documentación clara en la interfaz de OpenAPI/Swagger
- Filtrado de datos sensibles o innecesarios
Para definir un modelo de respuesta, utilizamos el parámetro response_model
en el decorador de ruta:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class User(BaseModel):
id: int
username: str
email: str
password: str # Campo sensible que no queremos exponer
is_active: bool = True
class UserResponse(BaseModel):
id: int
username: str
is_active: bool
@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
# Simulamos obtener un usuario de la base de datos
user = User(
id=user_id,
username="usuario_ejemplo",
email="usuario@ejemplo.com",
password="contraseña_secreta",
is_active=True
)
return user # FastAPI filtrará automáticamente los campos
En este ejemplo, aunque la función devuelve un objeto User
completo con todos sus campos, FastAPI utilizará el modelo UserResponse
para filtrar la respuesta, eliminando automáticamente los campos sensibles como email
y password
.
Personalización de respuestas con include y exclude
FastAPI permite un control más granular sobre los campos que se incluyen o excluyen en la respuesta mediante los parámetros response_model_include
y response_model_exclude
:
@app.get(
"/users/{user_id}/include",
response_model=User,
response_model_include={"id", "username"}
)
def get_user_include(user_id: int):
return User(
id=user_id,
username="usuario_ejemplo",
email="usuario@ejemplo.com",
password="contraseña_secreta"
)
@app.get(
"/users/{user_id}/exclude",
response_model=User,
response_model_exclude={"password", "email"}
)
def get_user_exclude(user_id: int):
return User(
id=user_id,
username="usuario_ejemplo",
email="usuario@ejemplo.com",
password="contraseña_secreta"
)
Estos métodos son útiles para casos específicos, pero generalmente es mejor definir modelos de respuesta dedicados como hicimos con UserResponse
.
Respuestas con listas y tipos genéricos
Para devolver colecciones de objetos, podemos usar tipos genéricos como List
:
@app.get("/users/", response_model=List[UserResponse])
def get_users():
users = [
User(id=1, username="usuario1", email="u1@ejemplo.com", password="pass1"),
User(id=2, username="usuario2", email="u2@ejemplo.com", password="pass2"),
User(id=3, username="usuario3", email="u3@ejemplo.com", password="pass3")
]
return users # FastAPI filtrará cada elemento de la lista
Respuestas con campos opcionales
A veces necesitamos modelos de respuesta con campos que pueden estar presentes o no:
class ItemBase(BaseModel):
name: str
description: Optional[str] = None
price: float
class ItemResponse(ItemBase):
id: int
tax: Optional[float] = None
@app.get("/items/{item_id}", response_model=ItemResponse)
def get_item(item_id: int, include_tax: bool = False):
item = {
"id": item_id,
"name": "Smartphone",
"description": "Último modelo",
"price": 799.99
}
if include_tax:
item["tax"] = item["price"] * 0.21
return item
En este ejemplo, el campo tax
solo se incluirá en la respuesta si el parámetro de consulta include_tax
es True
.
Personalización avanzada con modelos anidados
Los modelos de respuesta pueden contener estructuras anidadas complejas:
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class UserProfile(BaseModel):
bio: Optional[str] = None
interests: List[str] = []
class UserDetailResponse(BaseModel):
id: int
username: str
is_active: bool
address: Optional[Address] = None
profile: UserProfile
@app.get("/users/{user_id}/details", response_model=UserDetailResponse)
def get_user_details(user_id: int):
return {
"id": user_id,
"username": "usuario_detallado",
"is_active": True,
"address": {
"street": "Calle Principal 123",
"city": "Madrid",
"country": "España",
"postal_code": "28001"
},
"profile": {
"bio": "Desarrollador de software",
"interests": ["Python", "FastAPI", "Desarrollo web"]
}
}
Respuestas con uniones de tipos
A veces una ruta puede devolver diferentes tipos de respuestas según ciertas condiciones:
from typing import Union
class Message(BaseModel):
message: str
@app.get("/items/{item_id}", response_model=Union[ItemResponse, Message])
def get_item_or_message(item_id: int):
if item_id == 0:
return {"message": "Este es un ítem especial"}
return {
"id": item_id,
"name": "Producto regular",
"price": 20.0
}
Sin embargo, este enfoque puede complicar la documentación y el consumo de la API. Es preferible mantener tipos de respuesta consistentes y usar códigos de estado para diferenciar situaciones.
Respuestas personalizadas con JSONResponse
Para casos donde necesitamos un control total sobre la respuesta, podemos usar JSONResponse
directamente:
from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/custom-response/")
def get_custom_response():
content = {
"message": "Esta es una respuesta personalizada",
"data": {
"items": [1, 2, 3],
"total": 3
}
}
return JSONResponse(
content=content,
status_code=status.HTTP_200_OK,
headers={"X-Custom-Header": "valor-personalizado"}
)
Respuestas con diferentes formatos
FastAPI permite devolver respuestas en diferentes formatos además de JSON:
from fastapi.responses import PlainTextResponse, HTMLResponse, RedirectResponse
@app.get("/text/", response_class=PlainTextResponse)
def get_text():
return "Este es un texto plano"
@app.get("/html/", response_class=HTMLResponse)
def get_html():
return """
<html>
<head>
<title>Respuesta HTML</title>
</head>
<body>
<h1>Hola desde FastAPI</h1>
<p>Esta es una respuesta HTML</p>
</body>
</html>
"""
@app.get("/redirect/")
def redirect():
return RedirectResponse(url="/text/")
Respuestas con archivos
Para devolver archivos, podemos usar FileResponse
:
from fastapi.responses import FileResponse
@app.get("/download/report/", response_class=FileResponse)
def download_report():
report_path = "reports/informe.pdf"
filename = "informe-mensual.pdf"
return FileResponse(
path=report_path,
filename=filename,
media_type="application/pdf"
)
Personalización de respuestas de error
Podemos personalizar cómo se manejan los errores en nuestra API:
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"status": "error",
"message": "Error de validación en los datos",
"details": exc.errors(),
"data": exc.body
}
)
class Item(BaseModel):
name: str
price: float
quantity: int
@app.post("/items/")
def create_item(item: Item):
return item
En este ejemplo, personalizamos cómo se presentan los errores de validación, proporcionando un formato más amigable y consistente.
Documentación mejorada con ejemplos
Podemos mejorar la documentación de nuestra API añadiendo ejemplos a nuestros modelos:
from pydantic import BaseModel, Field
from typing import List
class Product(BaseModel):
id: int
name: str = Field(..., example="Smartphone")
description: str = Field(..., example="Último modelo con cámara de alta resolución")
price: float = Field(..., example=799.99)
tags: List[str] = Field(default=[], example=["electrónica", "móvil"])
class Config:
schema_extra = {
"example": {
"id": 1,
"name": "Laptop",
"description": "Portátil de alto rendimiento",
"price": 1299.99,
"tags": ["electrónica", "computadora", "trabajo"]
}
}
@app.get("/products/{product_id}", response_model=Product)
def get_product(product_id: int):
# Lógica para obtener el producto
return {
"id": product_id,
"name": "Tablet",
"description": "Tablet de 10 pulgadas",
"price": 349.99,
"tags": ["electrónica", "tablet"]
}
Hay dos formas de añadir ejemplos: usando el parámetro example
en Field
para campos individuales, o definiendo un ejemplo completo en schema_extra
dentro de la clase Config
.
Respuestas condicionales basadas en parámetros
Podemos adaptar dinámicamente nuestras respuestas según los parámetros recibidos:
from fastapi import FastAPI, Query
from typing import Optional
app = FastAPI()
@app.get("/products/")
def get_products(
skip: int = 0,
limit: int = 10,
category: Optional[str] = None,
include_details: bool = False
):
# Base de productos simulada
products = [
{"id": i, "name": f"Producto {i}", "price": 10.0 * i}
for i in range(1, 20)
]
# Filtrar por categoría si se especifica
if category:
products = [p for p in products if p.get("category") == category]
# Aplicar paginación
result = products[skip:skip+limit]
# Añadir detalles si se solicitan
if include_details:
for product in result:
product["details"] = {
"stock": 100 - product["id"],
"rating": 4.5,
"reviews_count": product["id"] * 5
}
# Construir respuesta con metadatos
response = {
"items": result,
"total": len(products),
"limit": limit,
"skip": skip
}
return response
Este enfoque permite crear APIs flexibles que pueden adaptarse a diferentes necesidades de los clientes sin tener que crear múltiples endpoints.
Ejercicios de esta lección Respuestas y códigos de estado
Evalúa tus conocimientos de esta lección Respuestas y códigos de estado con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Todas las lecciones de FastAPI
Accede a todas las lecciones de FastAPI y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Fastapi Y Configuración
Introducción Y Entorno
Respuestas Y Códigos De Estado
Api Rest
Validación De Datos Con Pydantic 2
Api Rest
Rutas Y Parámetros
Api Rest
Conexión De Fastapi Con Sqlalchemy
Persistencia
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la clasificación y significado de los códigos de estado HTTP en APIs REST.
- Implementar códigos de estado HTTP adecuados en rutas de FastAPI usando el parámetro status_code.
- Utilizar la clase HTTPException para manejar errores y respuestas con códigos específicos.
- Definir y personalizar modelos de respuesta para controlar la estructura y contenido de las respuestas.
- Aplicar buenas prácticas para diseñar respuestas claras, consistentes y seguras en APIs REST con FastAPI.