di

Resumen

En este tutorial se muestra el desarrollo de una API REST para la gestión de ofertas flash utilizando FastAPI y Redis. La API permite crear, obtener, reservar y eliminar ofertas flash, almacenando los datos en Redis. Se utiliza Docker y Docker Compose para configurar el entorno de desarrollo, y la librería redis-py para interactuar con Redis desde la aplicación FastAPI. A lo largo del tutorial, se describen los pasos necesarios para configurar el entorno, implementar la API y probar los endpoints utilizando Postman.

Objetivos
  • Desarrollar una API REST utilizando FastAPI y Redis para gestionar ofertas flash.

  • Implementar operaciones CRUD (Crear, Obtener, Reservar, Eliminar) para las ofertas flash.

  • Configurar un entorno de desarrollo utilizando Docker y Docker Compose.

  • Utilizar redis-py para interactuar con Redis desde la aplicación FastAPI.

  • Garantizar la consistencia de los datos en entornos de alta concurrencia mediante operaciones atómicas en Redis.

  • Probar los endpoints de la API utilizando Postman.

Este proyecto es una adaptación del tutorial SlimRedisAPIOfertasFlash, en el que se utilizaba Slim Framework y PHPRedis. En este caso, se utiliza FastAPI y redis-py para Python.

El código de este proyecto se encuentra disponible en el repositorio FastAPIRedisAPIOfertasFlash.

1. Introducción

Las ofertas flash son una estrategia clave en el comercio electrónico moderno. Empresas como Amazon, AliExpress o eBay utilizan este tipo de promociones para generar urgencia y aumentar las ventas en períodos de tiempo reducidos. Sin embargo, la gestión eficiente de estas ofertas supone un desafío técnico:

  • ¿Cómo aseguramos que el stock disponible se actualiza correctamente ante miles de reservas simultáneas?

  • ¿Cómo evitamos sobreventas o reservas incorrectas en un sistema de alta concurrencia?

  • ¿Cómo garantizamos que las ofertas expiren automáticamente cuando finaliza su tiempo de validez?

Redis es una excelente opción para abordar estos problemas gracias a su velocidad, capacidad de manejo de operaciones atómicas y soporte para la expiración automática de datos. En esta tarea, pondrás en práctica estas capacidades diseñando una API REST que administre ofertas flash en un entorno de comercio electrónico.

2. Descripción del problema

En este proyecto, se va a desarrollar una API REST para gestionar ofertas flash en una tienda online utilizando FastAPI, Redis y redis-py para interactuar con Redis desde FastAPI. La API debe permitir:

  • Crear una nueva oferta flash con un producto, un descuento, un stock y un tiempo de expiración.

  • Obtener detalles de una oferta flash, incluyendo el tiempo restante para que la oferta expire.

  • Reservar un producto en oferta, disminuyendo el stock disponible y añadiendo la reserva a la lista de reservas.

  • Obtener la lista de usuarios con reserva en una oferta flash.

  • Eliminar una oferta flash, eliminando también las reservas asociadas.

Se usarán estructuras de datos de Redis como hashes y listas para almacenar los datos de las ofertas y las reservas. También se usará la expiración automática de claves en Redis para gestionar el tiempo de validez de las ofertas, y las operaciones atómicas de Redis para garantizar la consistencia de los datos en entornos de alta concurrencia.

3. Configuración del entorno de desarrollo

Para configurar el entorno de desarrollo de la API REST, utilizaremos Docker y Docker Compose. El archivo docker-compose.yml define los servicios necesarios para ejecutar la aplicación, incluyendo Redis, Python con FastAPI y Nginx.

A continuación se muestra el contenido del archivo docker-compose.yml:

version: "3.8"

services:
  redis:
    container_name: redis_fastapi
    image: "redis/redis-stack:6.2.6-v2"
    restart: always
    ports:
      - "6379:6379"
      - "8001:8001"
    volumes:
      - "./redis-data:/data"

  fastapi:
    container_name: fastapi_app
    build:
      context: .
      dockerfile: docker/python/Dockerfile
    ports:
      - "8000:8000"
    environment:
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    volumes:
      - .:/app
    depends_on:
      - redis
    restart: always

  nginx:
    container_name: fastapi_nginx
    image: nginx:stable-alpine
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - fastapi
    restart: always

3.1. Servicios

  • Redis: Servicio de base de datos en memoria que se utiliza para almacenar las ofertas flash y las reservas. Se expone en los puertos 6379 (servidor Redis) y 8001 (Redis Insight, una interfaz web gráfica para Redis). Incluye un volumen que mapea el directorio ./redis-data al directorio de datos de Redis.

  • FastAPI: Servicio que ejecuta la aplicación FastAPI con Uvicorn. Se construye a partir del contexto raíz usando docker/python/Dockerfile y se expone en el puerto 8000. Incluye un volumen que mapea el directorio actual al directorio de trabajo de la aplicación.

  • Nginx: Servidor web que actúa como proxy inverso hacia la aplicación FastAPI. Se expone en el puerto 80 y depende del servicio FastAPI.

Configuración del contenedor Python/FastAPI

Para ejecutar FastAPI en el contenedor, es necesario construir una imagen Docker con Python y las dependencias necesarias. A continuación se muestra el contenido del archivo docker/python/Dockerfile:

FROM python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt (1)

COPY . .

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] (2)
1 Instala las dependencias definidas en requirements.txt.
2 Arranca la aplicación FastAPI con Uvicorn en modo recarga automática.

Las dependencias de la aplicación se definen en el archivo requirements.txt:

fastapi==0.111.0
uvicorn[standard]==0.29.0
redis==5.0.4
pydantic==2.7.1

Uvicorn es el servidor ASGI que ejecuta la aplicación FastAPI. La opción --reload permite la recarga automática del servidor cuando se detectan cambios en el código fuente, lo que es muy útil durante el desarrollo.

3.2. Instrucciones de instalación

Para instalar y ejecutar la aplicación, sigue estos pasos:

  1. Clona el repositorio:

    git clone https://github.com/ualmtorres/FastAPIRedisAPIOfertasFlash.git
    cd FastAPIRedisAPIOfertasFlash
  2. Ejecuta Docker Compose para levantar los servicios:

    docker-compose up -d --build
  3. Accede a la documentación de la API REST en http://localhost.

  4. Accede a los endpoints de la API REST en http://localhost/api.

  5. Accede a la documentación interactiva Swagger UI en http://localhost:8000/docs.

Con estos pasos, tendrás el entorno de desarrollo configurado y la API REST en funcionamiento.

4. Uso básico de redis-py

redis-py es la librería oficial de Python para interactuar con Redis. A continuación, se muestra cómo instalar y utilizar redis-py en un entorno de desarrollo.

Más información sobre redis-py en la documentación oficial.

4.1. Conexión a Redis

Para conectarse a un servidor Redis, se crea una instancia de redis.Redis indicando el host y el puerto. En este proyecto, la conexión se centraliza en el módulo app/redis_client.py:

import redis
import os

def get_redis_client() -> redis.Redis:
    return redis.Redis(
        host=os.getenv("REDIS_HOST", "redis"),
        port=int(os.getenv("REDIS_PORT", 6379)),
        decode_responses=True (1)
    )
1 El parámetro decode_responses=True indica que las respuestas de Redis se decodifican automáticamente como cadenas de texto Python. El valor de REDIS_HOST se obtiene de la variable de entorno definida en el archivo docker-compose.yml. Su valor por defecto es redis, que coincide con el nombre del servicio Redis en Docker Compose.

4.2. Operaciones básicas

4.2.1. Establecer y obtener valores

Puedes establecer un valor en Redis utilizando el método set y obtenerlo utilizando el método get:

redis_client.set('key', 'value')
value = redis_client.get('key')
print(value)  # Output: value

4.2.2. Incrementar y decrementar valores

Puedes incrementar y decrementar valores numéricos utilizando los métodos incr y decr:

redis_client.set('counter', 1)
redis_client.incr('counter')
print(redis_client.get('counter'))  # Output: 2
redis_client.decr('counter')
print(redis_client.get('counter'))  # Output: 1

4.2.3. Trabajar con listas

Puedes añadir elementos a una lista utilizando los métodos lpush y rpush, y obtener elementos utilizando lrange:

redis_client.lpush('list', 'value1')
redis_client.rpush('list', 'value2')
list_items = redis_client.lrange('list', 0, -1)
print(list_items)  # Output: ['value1', 'value2']

4.2.4. Trabajar con hashes

Puedes establecer y obtener campos en un hash utilizando los métodos hset y hget, y obtener todos los campos con hgetall:

redis_client.hset('hash', mapping={'field1': 'value1', 'field2': 'value2'})
field1 = redis_client.hget('hash', 'field1')
field2 = redis_client.hget('hash', 'field2')
print(field1)  # Output: value1
print(field2)  # Output: value2

4.3. Manejo de errores

Es importante manejar los errores al trabajar con Redis. Puedes utilizar bloques try-except para capturar excepciones:

import redis

try:
    redis_client = redis.Redis(host='redis', port=6379, decode_responses=True)
    redis_client.ping()
except redis.ConnectionError as e:
    print(f'Error de conexión: {e}')

Con estos conceptos básicos, puedes comenzar a utilizar redis-py para interactuar con Redis en tus aplicaciones Python.

5. Desarrollo de la API

A continuación pasamos a desarrollar la API REST para gestionar ofertas flash con FastAPI y Redis. Comenzaremos por definir la estructura de la API y luego implementaremos los endpoints necesarios para crear, obtener, reservar y eliminar ofertas flash. Utilizaremos redis-py para interactuar con Redis y almacenar los datos de las ofertas y las reservas.

5.1. Estructura del proyecto

El proyecto se organiza en los siguientes archivos dentro de la carpeta app/:

app/
├── __init__.py
├── main.py          # Aplicación FastAPI y definición de endpoints
├── models.py        # Modelos Pydantic para validación de datos
└── redis_client.py  # Cliente Redis

5.2. Modelos de datos

FastAPI utiliza modelos Pydantic para validar y documentar automáticamente los datos de entrada. Los modelos se definen en app/models.py:

from pydantic import BaseModel

class OfferCreate(BaseModel):
    product_id: str
    discount: str
    stock: int
    expires_in: int  # segundos hasta que expira la oferta

class Reservation(BaseModel):
    user_id: str

Una ventaja de FastAPI sobre Slim Framework es que los modelos Pydantic permiten validar automáticamente los datos de entrada y generar documentación interactiva (Swagger UI y ReDoc) sin ninguna configuración adicional.

5.3. Especificación de los endpoints de la API

A continuación se describen los endpoints disponibles en la API REST para gestionar ofertas flash:

  • POST /api/offers: Crear una nueva oferta.

  • GET /api/offers/{product_id}: Obtener detalles de una oferta.

  • POST /api/offers/{product_id}/reserve: Reservar un producto en oferta.

  • GET /api/offers/{product_id}/reservations: Obtener la lista de usuarios con reserva en una oferta.

  • DELETE /api/offers/{product_id}: Eliminar una oferta.

5.3.1. Ejemplo de JSON para crear una oferta

{
    "product_id": "123",
    "discount": "30",
    "stock": 100,
    "expires_in": 3600
}

5.3.2. Ejemplo de JSON para crear una reserva

{
    "user_id": "456"
}

5.4. Implementación de la API

En esta sección vamos a implementar la API REST para gestionar ofertas flash utilizando FastAPI y Redis. Seguiremos un enfoque incremental, comenzando con la configuración general y luego desarrollando cada endpoint paso a paso.

5.4.1. Crear la aplicación FastAPI

Crea un archivo app/main.py y añade el siguiente código para configurar la aplicación FastAPI y conectar a Redis:

import time
from contextlib import asynccontextmanager
from pathlib import Path

import redis.exceptions
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse

from app.models import OfferCreate, Reservation
from app.redis_client import get_redis_client

redis_client = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global redis_client
    redis_client = get_redis_client() (1)
    yield
    redis_client.close()

app = FastAPI(title="API Ofertas Flash", version="1.0.0", lifespan=lifespan)

@app.get("/", response_class=HTMLResponse)
def root():
    html_path = Path(__file__).parent.parent / "public" / "index.html"
    return html_path.read_text(encoding="utf-8") (2)
1 El cliente Redis se inicializa durante el arranque de la aplicación mediante el gestor de ciclo de vida lifespan, garantizando que la conexión esté disponible antes de atender peticiones y se cierre limpiamente al detener la app.
2 Devuelve el contenido del archivo index.html en la ruta raíz. Este archivo se utiliza para mostrar la documentación básica de la API REST.

A diferencia de Slim Framework, FastAPI genera automáticamente documentación interactiva en /docs (Swagger UI) y /redoc (ReDoc). Esto permite explorar y probar la API directamente desde el navegador sin necesidad de herramientas externas.

5.4.2. Endpoint de prueba

Añade el siguiente código para definir un endpoint de prueba GET /api/test que devuelve un mensaje de prueba:

@app.get("/api/test")
def test():
    return {"status": 200, "message": "API is working"}

En FastAPI, devolver un diccionario desde una función de ruta equivale a devolver una respuesta JSON automáticamente serializada. No es necesaria ninguna función auxiliar como createJsonResponse de Slim Framework.

5.4.3. Endpoint para crear una nueva oferta

Añade el siguiente código para definir el endpoint POST /api/offers que permite crear una nueva oferta. Este endpoint recibe los datos de la oferta en el cuerpo de la solicitud y los almacena en Redis. En el cuerpo de la solicitud, se deben proporcionar los siguientes campos: product_id, discount, stock y expires_in. El campo expires_in representa el tiempo de expiración de la oferta en segundos. El endpoint devuelve un mensaje de éxito junto con los detalles de la oferta creada. Redis almacena los datos de la oferta en un hash con la clave offer:{product_id}. La operación Redis que se usaría para almacenar los datos de la oferta sería similar a la siguiente:

HSET offer:123 product_id 123 discount 30 stock 100 expires_at 1717609200

expires_at es un campo calculado que representa el tiempo de expiración de la oferta en formato UNIX timestamp. Se calcula sumando el tiempo actual en segundos al tiempo de expiración en segundos proporcionado en la solicitud.

@app.post("/api/offers", status_code=200)
def create_offer(offer: OfferCreate):
    offer_key = f"offer:{offer.product_id}"
    expires_at = int(time.time()) + offer.expires_in

    redis_client.hset(offer_key, mapping={ (1)
        "product_id": offer.product_id,
        "discount":   offer.discount,
        "stock":      offer.stock,
        "expires_at": expires_at,
    })
    redis_client.expire(offer_key, offer.expires_in)

    return {
        "status": 200,
        "message": "Offer created successfully",
        "offer": {
            "product_id": offer.product_id,
            "discount":   offer.discount,
            "stock":      offer.stock,
            "expires_at": expires_at,
        }
    }
1 El método hset con el parámetro mapping permite establecer múltiples campos de un hash en una sola llamada, equivalente a las múltiples llamadas a hSet de PHPRedis.

5.4.4. Endpoint para obtener detalles de una oferta

Añade el siguiente código para definir el endpoint GET /api/offers/{product_id} que permite obtener detalles de una oferta. Este endpoint recibe el product_id de la oferta como parámetro y devuelve los detalles de la oferta junto con el tiempo restante para que la oferta expire. Redis almacena los datos de la oferta en un hash con la clave offer:{product_id}. La operación Redis que se usaría para obtener los detalles de la oferta sería similar a la siguiente:

HGETALL offer:123
@app.get("/api/offers/{product_id}")
def get_offer(product_id: str):
    offer_key = f"offer:{product_id}"

    if not redis_client.exists(offer_key):
        raise HTTPException(status_code=404, detail="Offer not found") (1)

    offer          = redis_client.hgetall(offer_key)
    time_remaining = redis_client.ttl(offer_key)

    return {
        "status": 200,
        "offer":  offer,
        "time_remaining": time_remaining,
    }
1 En FastAPI, se lanza una excepción HTTPException para devolver respuestas de error. FastAPI la captura automáticamente y devuelve la respuesta HTTP correcta con el código de estado indicado.

5.4.5. Endpoint para reservar un producto en oferta

Añade el siguiente código para definir el endpoint POST /api/offers/{product_id}/reserve que permite reservar un producto en oferta. Este endpoint recibe el product_id en la URL y el user_id a través del modelo Reservation. Comprueba si la oferta tiene stock disponible y, si es así, disminuye el stock y añade la reserva a la lista de reservas usando una transacción optimista con WATCH. La operación Redis equivalente sería:

WATCH offer:123
stock = HGET offer:123 stock
if stock > 0
    MULTI
    HINCRBY offer:123 stock -1
    LPUSH offer:123:reservations user_456
    EXEC

La operación WATCH se utiliza para garantizar la atomicidad de la transacción. En redis-py, esto se implementa usando el gestor de contexto pipeline() con pipe.watch(), que es equivalente al uso de $redis→watch() seguido de $redis→multi() y $redis→exec() en PHPRedis.

@app.post("/api/offers/{product_id}/reserve")
def reserve_offer(product_id: str, reservation: Reservation):
    offer_key = f"offer:{product_id}"

    if not redis_client.exists(offer_key):
        raise HTTPException(status_code=404, detail="Offer not found")

    with redis_client.pipeline() as pipe: (1)
        try:
            pipe.watch(offer_key)
            stock = int(redis_client.hget(offer_key, "stock") or 0)

            if stock <= 0:
                raise HTTPException(status_code=400, detail="Out of stock")

            pipe.multi() (2)
            pipe.hincrby(offer_key, "stock", -1)
            pipe.lpush(f"{offer_key}:reservations", f"user_{reservation.user_id}")
            pipe.execute() (3)

            return {
                "status":          200,
                "message":         "Product reserved successfully",
                "remaining_stock": stock - 1,
            }

        except redis.exceptions.WatchError: (4)
            raise HTTPException(
                status_code=500,
                detail="Failed to reserve product, please try again"
            )
1 El gestor de contexto pipeline() agrupa las operaciones Redis en una transacción.
2 pipe.multi() inicia el bloque de transacción, equivalente a $redis→multi() en PHPRedis.
3 pipe.execute() ejecuta todas las operaciones en bloque de forma atómica, equivalente a $redis→exec() en PHPRedis.
4 redis.exceptions.WatchError se lanza si la clave vigilada ha sido modificada por otro cliente entre el WATCH y el EXEC, indicando un conflicto de concurrencia.

5.4.6. Endpoint para obtener la lista de usuarios con reserva en una oferta

Añade el siguiente código para definir el endpoint GET /api/offers/{product_id}/reservations que permite obtener la lista de usuarios con reserva en una oferta. Este endpoint recibe el product_id de la oferta como parámetro y devuelve la lista de usuarios con reserva. Redis almacena las reservas en una lista con la clave offer:{product_id}:reservations. La operación Redis que se usaría para obtener la lista de usuarios con reserva sería similar a la siguiente:

LRANGE offer:123:reservations 0 -1
@app.get("/api/offers/{product_id}/reservations")
def get_reservations(product_id: str):
    reservations_key = f"offer:{product_id}:reservations"

    if not redis_client.exists(reservations_key):
        raise HTTPException(
            status_code=404,
            detail="No reservations found for this offer"
        )

    reservations = redis_client.lrange(reservations_key, 0, -1)

    return {"status": 200, "reservations": reservations}

5.4.7. Endpoint para eliminar una oferta

Añade el siguiente código para definir el endpoint DELETE /api/offers/{product_id} que permite eliminar una oferta. Este endpoint recibe el product_id de la oferta como parámetro y elimina la oferta y las reservas asociadas en Redis. Redis almacena los datos de la oferta en un hash con la clave offer:{product_id} y las reservas en una lista con la clave offer:{product_id}:reservations. La operación Redis que se usaría para eliminar una oferta sería similar a la siguiente:

DEL offer:123
DEL offer:123:reservations
@app.delete("/api/offers/{product_id}")
def delete_offer(product_id: str):
    offer_key = f"offer:{product_id}"

    if not redis_client.exists(offer_key):
        raise HTTPException(status_code=404, detail="Offer not found")

    redis_client.delete(offer_key)
    redis_client.delete(f"{offer_key}:reservations")

    return {"status": 200, "message": "Offer deleted successfully"}

Con estos pasos, hemos implementado la API REST para gestionar ofertas flash utilizando FastAPI y Redis. Hemos configurado la aplicación, definido los modelos de datos, los endpoints necesarios y las operaciones CRUD sobre las ofertas flash.

5.5. Pruebas de la API

Una vez que hemos desarrollado la API REST con FastAPI, podemos probar los endpoints de dos formas:

  • Utilizando Postman con las peticiones habituales.

  • Utilizando la documentación interactiva generada automáticamente por FastAPI en http://localhost:8000/docs (Swagger UI).

A continuación se muestran algunos ejemplos de peticiones a la API REST:

  • POST /api/offers: Crear una nueva oferta añadiendo los datos siguientes en el cuerpo de la solicitud.

    {
        "product_id": "123",
        "discount": "30",
        "stock": 100,
        "expires_in": 3600
    }
    fastapi create offer
  • GET /api/offers/{product_id}: Obtener detalles de una oferta.

    fastapi get offer
  • POST /api/offers/{product_id}/reserve: Reservar un producto en oferta añadiendo los datos siguientes en el cuerpo de la solicitud.

    {
        "user_id": "456"
    }
    fastapi reserve product
  • GET /api/offers/{product_id}/reservations: Obtener la lista de usuarios con reserva en una oferta.

    fastapi get reservations
  • DELETE /api/offers/{product_id}: Eliminar una oferta.

    fastapi delete offer

En estos ejemplos, se han probado las operaciones CRUD en las ofertas flash utilizando la API REST con FastAPI y Redis. Las operaciones se han realizado con éxito y se han devuelto los resultados esperados.

6. Conclusiones

En este tutorial, se ha desarrollado una API REST para la gestión de ofertas flash utilizando FastAPI y Redis. A lo largo del proceso, hemos configurado un entorno de desarrollo con Docker y Docker Compose, implementado los endpoints necesarios para crear, obtener, reservar y eliminar ofertas flash, y probado la API utilizando Postman y la documentación interactiva de FastAPI.

Redis ha demostrado ser una excelente opción para manejar la alta concurrencia y la expiración automática de datos, lo que es crucial para la gestión eficiente de ofertas flash en un entorno de comercio electrónico. La combinación de FastAPI y redis-py ha permitido una implementación rápida y eficiente de la API, con las ventajas adicionales de la validación automática de datos mediante modelos Pydantic y la generación automática de documentación interactiva mediante Swagger UI y ReDoc.

En comparación con la implementación en Slim Framework, FastAPI ofrece:

  • Validación automática de los datos de entrada mediante modelos Pydantic, eliminando la necesidad de validar manualmente los campos del cuerpo de la solicitud.

  • Documentación interactiva generada automáticamente en /docs y /redoc, sin necesidad de herramientas externas.

  • Tipado estático en Python, que mejora la legibilidad y el mantenimiento del código.

  • Rendimiento asíncrono mediante ASGI y Uvicorn, lo que permite manejar un mayor número de solicitudes concurrentes.

Este proyecto no solo proporciona una solución práctica para la gestión de ofertas flash, sino que también sirve como una base sólida para futuros desarrollos y mejoras en aplicaciones de comercio electrónico.

Licencia

Licencia CC BY-NC-ND 4.0

Copyright (c) 2026 [Manuel Torres - Departamento de Informática - Universidad de Almería]

Este proyecto está licenciado bajo la Licencia CC BY-NC-ND 4.0. Esto significa que puedes compartir el proyecto siempre que cites al autor, no lo uses para fines comerciales y no realices obras derivadas.