Volver a Documentación
Ejemplos avanzados — Node.js
Patrones avanzados y casos de uso reales para integrar Rayuela en aplicaciones Node.js de producción.
Integración con Express.js
Middleware y rutas para aplicación e-commerce completa
const express = require('express');
const axios = require('axios');
const Redis = require('redis');
class RayuelaService {
constructor(apiKey, redisClient) {
this.apiKey = apiKey;
this.baseURL = 'https://rayuela-backend-e7apihrdoa-uc.a.run.app/api/v1';
this.redis = redisClient;
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
timeout: 10000
});
}
async getRecommendations(userId, options = {}) {
const cacheKey = `recs:${userId}:${JSON.stringify(options)}`;
// Intentar obtener del cache
try {
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
} catch (err) {
console.warn('Redis cache miss:', err.message);
}
// Obtener de la API
try {
const response = await this.client.post('/recommendations/personalized/query', {
external_user_id: userId,
recommendation_goal: 'user_engagement',
model_variant: 'standard',
limit: options.limit || 10,
filters: options.filters || { logic: 'and', filters: [] },
context: options.context || {},
strategy: options.strategy || 'hybrid',
include_explanation: false
});
const recommendations = response.data;
// Cachear por 10 minutos
try {
await this.redis.setex(cacheKey, 600, JSON.stringify(recommendations));
} catch (err) {
console.warn('Redis cache write failed:', err.message);
}
return recommendations;
} catch (error) {
console.error('Rayuela API error:', error.message);
return this.getFallbackRecommendations();
}
}
async trackInteraction(userId, productId, interactionType, value = 1.0) {
try {
await this.client.post('/interactions', {
external_user_id: userId,
external_product_id: productId,
interaction_type: interactionType,
value: value
});
} catch (error) {
console.error('Failed to track interaction:', error.message);
// No lanzar error - tracking no debe romper la experiencia
}
}
getFallbackRecommendations() {
// Retornar productos populares hardcodeados
return {
items: [
{ externalId: 'popular_1', score: 0.9 },
{ externalId: 'popular_2', score: 0.8 }
]
};
}
}
// Configuración
const app = express();
const redis = Redis.createClient({ url: process.env.REDIS_URL });
const rayuela = new RayuelaService(process.env.RAYUELA_API_KEY, redis);
app.use(express.json());Middleware de Tracking
Middleware para tracking automático de interacciones
// Middleware para tracking automático
const trackingMiddleware = (interactionType) => {
return async (req, res, next) => {
const originalSend = res.send;
res.send = function(data) {
// Tracking asíncrono después de enviar respuesta
setImmediate(async () => {
try {
const userId = req.user?.id;
const productId = req.params.productId || req.body.productId;
if (userId && productId) {
await rayuela.trackInteraction(userId, productId, interactionType);
}
} catch (error) {
console.error('Tracking error:', error.message);
}
});
return originalSend.call(this, data);
};
next();
};
};
// Rutas con tracking automático
app.get('/api/products/:productId',
trackingMiddleware('view'),
async (req, res) => {
const { productId } = req.params;
// Obtener producto de la base de datos
const product = await Product.findById(productId);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(product);
}
);
app.post('/api/cart/add',
trackingMiddleware('cart_add'),
async (req, res) => {
const { productId, quantity } = req.body;
const userId = req.user.id;
// Lógica del carrito
await Cart.addItem(userId, productId, quantity);
res.json({ success: true });
}
);
app.post('/api/orders',
trackingMiddleware('purchase'),
async (req, res) => {
const { items } = req.body;
const userId = req.user.id;
// Procesar orden
const order = await Order.create({ userId, items });
res.json(order);
}
);Rutas de Recomendaciones
Endpoints optimizados para diferentes contextos
// Recomendaciones para homepage
app.get('/api/recommendations/homepage', async (req, res) => {
try {
const userId = req.user?.id;
if (!userId) {
// Usuario anónimo - mostrar productos populares
const popularProducts = await Product.find({ featured: true })
.limit(8)
.sort({ sales_count: -1 });
return res.json({
items: popularProducts.map(p => ({
externalId: p._id,
score: p.sales_count / 1000,
source: 'trending'
}))
});
}
const recommendations = await rayuela.getRecommendations(userId, {
limit: 8,
strategy: 'hybrid',
filters: {
logic: 'and',
filters: [{ field: 'inStock', op: 'eq', value: true }]
}
});
res.json(recommendations);
} catch (error) {
console.error('Homepage recommendations error:', error);
res.status(500).json({ error: 'Failed to get recommendations' });
}
});
// Recomendaciones para página de producto
app.get('/api/recommendations/product/:productId', async (req, res) => {
try {
const { productId } = req.params;
const userId = req.user?.id;
if (!userId) {
// Usuario anónimo - productos relacionados por categoría
const product = await Product.findById(productId);
const related = await Product.find({
category: product.category,
_id: { $ne: productId }
}).limit(4);
return res.json({
items: related.map(p => ({
externalId: p._id,
score: 0.8,
source: 'curated'
}))
});
}
const recommendations = await rayuela.getRecommendations(userId, {
limit: 4,
strategy: 'content_based',
filters: {
logic: 'and',
filters: [{ field: 'inStock', op: 'eq', value: true }]
},
context: {
page_type: 'product_detail',
source_external_product_id: productId
}
});
res.json(recommendations);
} catch (error) {
console.error('Product recommendations error:', error);
res.status(500).json({ error: 'Failed to get recommendations' });
}
});
// Recomendaciones para carrito
app.get('/api/recommendations/cart', async (req, res) => {
try {
const userId = req.user.id;
const cart = await Cart.findOne({ userId }).populate('items.product');
// Obtener categorías del carrito
const cartCategories = [...new Set(
cart.items.map(item => item.product.category)
)];
const recommendations = await rayuela.getRecommendations(userId, {
limit: 6,
strategy: 'hybrid',
filters: {
logic: 'and',
filters: [{
field: 'price',
op: 'lt',
value: Math.max(...cart.items.map(item => item.product.price)) * 1.5
}]
}
});
res.json(recommendations);
} catch (error) {
console.error('Cart recommendations error:', error);
res.status(500).json({ error: 'Failed to get recommendations' });
}
});Jobs en Background
Sincronización de datos con Bull Queue
const Bull = require('bull');
const syncQueue = new Bull('rayuela sync', process.env.REDIS_URL);
// Job para sincronizar productos
syncQueue.process('sync-products', async (job) => {
const { batchSize = 1000 } = job.data;
try {
let offset = 0;
let hasMore = true;
while (hasMore) {
const products = await Product.find({ is_active: true })
.skip(offset)
.limit(batchSize)
.lean();
if (products.length === 0) {
hasMore = false;
break;
}
// Transformar para Rayuela
const rayuelaProducts = products.map(p => ({
externalId: p._id.toString(),
name: p.name,
description: p.description,
price: p.price,
category: p.category,
brand: p.brand,
inStock: p.stock > 0,
attributes: p.attributes || {}
}));
// Enviar a Rayuela
await rayuela.client.post('/ingestion/batch', {
products: rayuelaProducts
});
offset += batchSize;
// Actualizar progreso
job.progress(Math.min(100, (offset / await Product.countDocuments()) * 100));
}
return { synced: offset };
} catch (error) {
console.error('Product sync failed:', error);
throw error;
}
});
// Job para sincronizar interacciones recientes
syncQueue.process('sync-interactions', async (job) => {
const { hoursBack = 24 } = job.data;
const cutoff = new Date(Date.now() - hoursBack * 60 * 60 * 1000);
try {
const interactions = await Interaction.find({
created_at: { $gte: cutoff }
}).lean();
const rayuelaInteractions = interactions.map(i => ({
external_user_id: i.user_id.toString(),
external_product_id: i.product_id.toString(),
interaction_type: i.type,
value: i.value || 1.0,
timestamp: i.created_at.toISOString()
}));
// Enviar en lotes
const batchSize = 1000;
for (let i = 0; i < rayuelaInteractions.length; i += batchSize) {
const batch = rayuelaInteractions.slice(i, i + batchSize);
await rayuela.client.post('/ingestion/batch', {
interactions: batch
});
}
return { synced: rayuelaInteractions.length };
} catch (error) {
console.error('Interaction sync failed:', error);
throw error;
}
});
// Programar jobs
syncQueue.add('sync-products', {}, {
repeat: { cron: '0 2 * * *' }, // Diario a las 2 AM
removeOnComplete: 5,
removeOnFail: 10
});
syncQueue.add('sync-interactions', { hoursBack: 1 }, {
repeat: { every: 60000 }, // Cada minuto
removeOnComplete: 10,
removeOnFail: 5
});
// Endpoint para trigger manual
app.post('/api/admin/sync-rayuela', async (req, res) => {
try {
const productJob = await syncQueue.add('sync-products', {});
const interactionJob = await syncQueue.add('sync-interactions', { hoursBack: 24 });
res.json({
productJobId: productJob.id,
interactionJobId: interactionJob.id
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});Optimización de Rendimiento
Mejores prácticas
- • Usa Redis para cachear recomendaciones (5-15 minutos)
- • Implementa circuit breaker para fallos de API
- • Tracking asíncrono con setImmediate() o queues
- • Connection pooling con axios.create()
- • Fallbacks para alta disponibilidad
⚠️ Consideraciones
- • Timeouts apropiados (10-30 segundos)
- • Rate limiting en endpoints públicos
- • Monitoreo de errores con Sentry/similar
- • Logs estructurados para debugging
Siguientes Pasos