Ejemplos de Uso

Aprende a conectar tu frontend con cualquier backend usando el BFF de Humalight

Los ejemplos funcionan con Strapi, APIs REST custom y más

Nota Importante

Los ejemplos a continuación son ilustrativos. Adapta las rutas y estructuras de datos según tu backend. Si usas Strapi, reemplaza tu-content-type con el nombre de tus content types reales.

🔍

Search API - Búsqueda unificada

API de búsqueda rápida basada en Typesense. Indexa places, events y posts. El modo minimal devuelve payloads compactos ideales para autosuggest y cards.

Búsqueda en todas las colecciones:

// Desde el frontend
const response = await fetch(
  '/api/search?q=fiesta&collection=all&limit=5',
  {
    headers: { 'x-api-key': process.env.NEXT_PUBLIC_BFF_API_KEY! },
    cache: 'no-store'
  }
);
const { data } = await response.json();
console.log(data); // minimal por defecto

Filtrar por colección específica:

// Solo eventos
const events = await fetch(
  '/api/search?q=rock&collection=events&limit=5',
  { headers: { 'x-api-key': process.env.NEXT_PUBLIC_BFF_API_KEY! }}
);

// Solo lugares
const places = await fetch(
  '/api/search?q=restaurante&collection=places',
  { headers: { 'x-api-key': process.env.NEXT_PUBLIC_BFF_API_KEY! }}
);

Implementar autosuggest en tu componente:

import { useState, useEffect } from 'react';

function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (!query) return;
    
    const timer = setTimeout(async () => {
      const res = await fetch(
        '/api/search?q=' + encodeURIComponent(query) + '&limit=5',
        { headers: { 'x-api-key': process.env.NEXT_PUBLIC_BFF_API_KEY! }}
      );
      const { data } = await res.json();
      setResults(data);
    }, 300); // debounce 300ms

    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)} 
        placeholder="Buscar lugares o eventos..." 
      />
      {results.map(item => (
        <div key={item.documentId}>
          {item.title} - {item.collection}
        </div>
      ))}
    </div>
  );
}

Actualizar índice de búsqueda (sync):

// Ejecuta después de agregar/editar contenido en tu base de datos
const response = await fetch('/api/search/sync', {
  method: 'POST',
  headers: { 'x-api-key': process.env.NEXT_PUBLIC_BFF_API_KEY! }
});
const result = await response.json();
console.log(result.message);

Respuesta minimal (places)

{
  collection: "places",
  documentId: "...",
  title: "...",
  portada: {
    url: "...",
    thumbnail: "...",
    medium: "...",
    large: "...",
    original: "..."
  },
  categories: [...],
  tags: [...],
  address: "...",
  phone: "...",
  horario: "..."
}

Respuesta minimal (events)

{
  collection: "events",
  documentId: "...",
  title: "...",
  portada: { ... },
  address: "...",
  fechasYHoras: "...",
  tieneCosto: true,
  costo: 100
}
1

Obtener Datos (GET)

Desde el Frontend:

// Llamar al BFF desde tu aplicación cliente
const response = await fetch('/api/strapi/tu-content-type');
const data = await response.json();
console.log(data.data);

Desde un Server Component:

import { httpClient } from '@/lib/http';

async function getData() {
  const response = await httpClient.get('/api/tu-endpoint');
  return response.data;
}

Con parámetros (ej: Strapi populate):

const response = await httpClient.get(
  '/api/tu-content-type?populate=*'
);
👥

Users API - Usuarios con Metadata

API personalizada que combina datos de usuarios con sus metadatos (usersmetas), incluyendo imágenes de perfil, direcciones y roles. Todos los datos PHP están deserializados y listos para usar.

Listar todos los usuarios:

// Obtener todos los usuarios con metadata procesada
const response = await fetch('/api/users');
const { data } = await response.json();

// Cada usuario incluye:
// - id, username, email, displayName, status
// - profileImage: { thumbnail, medium, large, original }
// - addresses: [{ tipo, calle, numero, colonia, ciudad, estado, cp }]
// - role: { id, name, description }
// - metas: { key: value } (todos los metadatos deserializados)

Filtrar por userId:

// Obtener usuario específico por ID
const response = await fetch('/api/users?userId=123');
const { data } = await response.json();

// Devuelve un solo usuario con toda su información

Filtrar por username o email:

// Por username
const response = await fetch('/api/users?username=johndoe');

// Por email
const response = await fetch('/api/users?email=john@example.com');

Filtrar por metaKey específica:

// Solo usuarios con foto de perfil
const response = await fetch('/api/users?metaKey=foto-de-perfil');

// Solo usuarios con dirección
const response = await fetch('/api/users?metaKey=direccion-principal');

Modo minimal (para listas):

// Respuesta optimizada sin metadatos completos
const response = await fetch('/api/users?minimal=true');
const { data } = await response.json();

// Solo incluye: id, username, email, displayName, profileImage (thumbnail)

Estructura de profileImage:

const { data: user } = await response.json();

// profileImage con múltiples variantes
console.log(user.profileImage);
/*
{
  thumbnail: "https://...150x150.jpg",
  medium: "https://...300x300.jpg",
  large: "https://...1024x1024.jpg",
  original: "https://...original.jpg"
}
*/

Componente React con usuarios:

async function UsersList() {
  const res = await fetch('/api/users?minimal=true', {
    next: { revalidate: 120 } // Cache por 2 minutos
  });
  
  const { data: users } = await res.json();
  
  return (
    <div className="grid grid-cols-4 gap-4">
      {users.map((user) => (
        <UserCard
          key={user.id}
          username={user.username}
          email={user.email}
          avatar={user.profileImage?.thumbnail}
          displayName={user.displayName}
        />
      ))}
    </div>
  );
}

✨ Características del Users API:

  • Deserialización automática de metadatos PHP
  • Extracción de variantes de imagen de perfil (thumbnail, medium, large)
  • Normalización de direcciones desde múltiples formatos
  • Filtrado flexible por ID, username, email o metaKey
  • Modo minimal optimizado para listas de usuarios
  • Incluye información de roles y permisos

Places API - Lectura de Lugares

API personalizada de lectura. Agrega datos de múltiples colecciones de Strapi (wx-posts + wx-postmetas) para generar lugares unificados.

GETListar y Consultar Lugares

Listar todos los lugares (datos mínimos para Cards):

// Respuesta optimizada para Cards
const response = await fetch('/api/places?minimal=true');
const { data } = await response.json();

// Cada lugar incluye:
// - documentId, post_type, post_name, title
// - portada: { thumbnail, medium, large, original, url }
// - categories: [{ name, slug }]
// - tags: [{ name }]
// - address, phone, horario (parseados y listos para usar)

Filtrar por tipo de post:

// Solo lugares (no eventos)
const response = await fetch('/api/places?postType=lugares&minimal=true');

// Solo eventos
const response = await fetch('/api/places?postType=eventos&minimal=true');

Obtener un lugar específico (por documentId o slug):

// Por documentId
const response = await fetch(
  '/api/places/fbjs6raaipdo07jbkvprrb7s?minimal=true'
);

// Por slug (post_name)
const response = await fetch(
  '/api/places/mi-bodega-aurrera-flamingos?minimal=true'
);

const { data } = await response.json();
// data contiene un solo lugar con todos sus datos

✅ Características del Places API:

  • GET: Lectura de lugares con datos agregados
  • Deserialización automática de metadatos
  • Información de ubicación, horarios y costos
  • Imágenes destacadas y galerías
  • Filtrado por categorías y términos
🎉

Events API - Eventos con Cache

API de eventos que agrega datos de WordPress/Strapi con información completa: fechas, ubicación, costos, imágenes y más.

Listar eventos (versión minimal para cards):

// Obtener eventos con datos optimizados para tarjetas
const response = await fetch('/api/events?minimal=true');
const { data, count } = await response.json();

// Cada evento incluye (minimal):
// - documentId: identificador único
// - title: título del evento
// - portada: { id, name, thumbnail, medium, large, original, url }
// - address: dirección completa del evento
// - fechasYHoras: fechas y horarios formateados
// - tieneCosto: boolean si tiene costo
// - costo: precio del evento (number)
console.log(data[0]);
/*
{
  documentId: "abc123",
  title: "Concierto de Rock",
  portada: {
    id: 45,
    name: "concierto.jpg",
    medium: "https://...medium.jpg",
    large: "https://...large.jpg"
  },
  address: "Av. Principal 123, Centro, CDMX",
  fechasYHoras: "2025-12-20 19:00, 2025-12-21 19:00",
  tieneCosto: true,
  costo: 350.50
}
*/

Obtener evento específico:

// Por documentId o slug
const response = await fetch('/api/events/concierto-rock-2025?minimal=true');
const { data } = await response.json();

// Devuelve un solo evento con estructura minimal

Datos completos (sin minimal):

// Respuesta completa con todos los campos
const response = await fetch('/api/events');
const { data } = await response.json();

// Incluye campos adicionales:
// - id, slug, excerpt, content, status, type
// - createdAt, updatedAt
// - author: { id, username, email }
// - logotipo: { id, name, thumbnail, medium, large, ... }
// - galeria: [{ id, name, thumbnail, medium, ... }]
// - categories: [{ id, documentId, name, slug }]

Filtrar por categoría/término:

// Solo eventos de música
const response = await fetch(
  '/api/events?termFilter=musica&minimal=true'
);

Componente React con eventos:

async function EventsList() {
  const res = await fetch('/api/events?minimal=true', {
    next: { revalidate: 300 } // Cache por 5 minutos
  });
  
  const { data: events } = await res.json();
  
  return (
    <div className="grid grid-cols-3 gap-4">
      {events.map((event) => (
        <EventCard
          key={event.documentId}
          title={event.title}
          image={event.portada?.large}
          address={event.address}
          dates={event.fechasYHoras}
          price={event.tieneCosto ? `$${event.costo}` : 'Gratis'}
        />
      ))}
    </div>
  );
}

✨ Características del Events API:

  • Deserialización automática de datos PHP de WordPress
  • Extracción de múltiples variantes de imágenes (thumbnail, medium, large)
  • Soporte para eventos gratuitos y de pago
  • Fechas y horarios formateados listos para mostrar
  • Cache de 5 minutos (configurable)
  • Versión minimal optimizada para listas y cards
🕒

Recent API - Posts Recientes con Redis

API que obtiene los posts más recientes de cualquier post_type. Reutiliza los agregadores de eventos y lugares. Cache con Redis (TTL: 5 minutos).

Obtener posts recientes (todos los tipos):

// 10 posts más recientes (default)
const response = await fetch('/api/recent');
const { data, count } = await response.json();

// Respuesta incluye posts de cualquier tipo
// Para eventos y lugares, usa sus agregadores específicos
// Para otros tipos (post, page), devuelve estructura genérica

Filtrar por post_types específicos:

// Solo eventos más recientes
const response = await fetch('/api/recent?postTypes=eventos&limit=5');

// Eventos y lugares
const response = await fetch('/api/recent?postTypes=eventos,lugares&limit=10');

// Con modo minimal para cards
const response = await fetch(
  '/api/recent?postTypes=eventos,lugares&minimal=true&limit=15'
);

Invalidar cache desde el frontend:

// 1. Invalidar todo el cache
await fetch('/api/recent', { method: 'POST' });

// 2. Invalidar solo eventos (todas las variaciones)
await fetch('/api/recent', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    postTypes: ['eventos']
  })
});

// 3. Invalidar key específica
await fetch('/api/recent', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    postTypes: ['eventos'],
    limit: 10,
    minimal: true
  })
});

Ejemplo: Invalidar cache al crear post:

async function handleCreatePost(postType, postData) {
  try {
    // Crear el post
    const res = await fetch('/api/strapi/wx-posts', {
      method: 'POST',
      body: JSON.stringify(postData)
    });

    if (res.ok) {
      // Invalidar cache de recent para ese postType
      await fetch('/api/recent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          postTypes: [postType]
        })
      });
      
      console.log(`✅ Cache invalidado para ${postType}`);
    }
  } catch (error) {
    console.error('Error:', error);
  }
}

// Uso
await handleCreatePost('eventos', { title: 'Nuevo Evento', ... });

Componente React con recent posts:

async function RecentPostsList() {
  const res = await fetch(
    '/api/recent?postTypes=eventos,lugares&limit=10&minimal=true',
    { next: { revalidate: 300 } } // Cache por 5 minutos
  );
  
  const { data: posts } = await res.json();
  
  return (
    <div className="grid grid-cols-2 gap-4">
      {posts.map((post) => (
        <PostCard
          key={post.documentId}
          title={post.title}
          image={post.portada?.large}
          type={post.post_type || 'post'}
        />
      ))}
    </div>
  );
}

🚀 Características del Recent API:

  • Reutiliza agregadores de eventos y lugares (datos completos)
  • Soporte para múltiples post_types simultáneos
  • Cache granular en Redis por combinación de parámetros
  • 3 niveles de invalidación: total, por tipo, o específica
  • Límite configurable (default: 10, max: 50)
  • Ordenamiento por fecha de creación (más reciente primero)
  • Perfecto para feeds de noticias y dashboards
🧭

Menus API - Menús de Navegación con Redis

API de menús con cache en Redis para respuestas ultra rápidas. Agrega datos de WordPress/Strapi y construye árboles jerárquicos de navegación.

Listar todos los menús disponibles:

// Obtener lista de todos los menús
const response = await fetch('/api/menus');
const { data, count } = await response.json();

// Cada menú incluye:
// - documentId: identificador único
// - name: nombre del menú
// - slug: slug normalizado
// - items: árbol jerárquico de elementos
console.log(data);
/*
[
  {
    documentId: "abc123",
    name: "Desktop Header",
    slug: "desktop-header",
    items: [
      {
        documentId: "item1",
        title: "Inicio",
        url: "/",
        order: 1,
        children: [...],
        metas: { menu_icon_url: "https://..." }
      }
    ]
  }
]
*/

Obtener un menú específico por nombre:

// Obtener menú por nombre o slug
const response = await fetch('/api/menus/desktop-header');
const { data } = await response.json();

// Menú con estructura jerárquica completa
console.log(data.items); // Array de items raíz con children anidados

Con array plano de items (útil para búsquedas):

// Incluir flatItems además del árbol jerárquico
const response = await fetch('/api/menus/mobile-menu?includeFlat=true');
const { data } = await response.json();

// data.items: árbol jerárquico
// data.flatItems: array plano con todos los items
const allUrls = data.flatItems.map(item => item.url);

Deshabilitar cache Redis (forzar fetch):

// Omitir cache y obtener datos frescos desde Strapi
const response = await fetch('/api/menus/desktop-header?disableCache=true');
const { data } = await response.json();

Invalidar cache (para webhooks de Strapi):

// Llamar cuando se actualice un menú en Strapi
const response = await fetch('/api/menus', {
  method: 'POST'
});
const { success, message } = await response.json();
// { success: true, message: "Menu cache invalidated successfully" }

Ejemplo de componente React con menú:

'use client';
import { useEffect, useState } from 'react';

export function NavigationMenu() {
  const [menu, setMenu] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadMenu() {
      const res = await fetch('/api/menus/desktop-header');
      const { data } = await res.json();
      setMenu(data);
      setLoading(false);
    }
    loadMenu();
  }, []);

  if (loading) return <div>Cargando menú...</div>;
  if (!menu) return null;

  return (
    <nav>
      {menu.items.map(item => (
        <a key={item.documentId} href={item.url}>
          {item.title}
          {/* Renderizar children recursivamente */}
        </a>
      ))}
    </nav>
  );
}

⚡ Ventajas del Cache con Redis:

  • Respuestas < 10ms desde Redis (vs ~200ms desde Strapi)
  • Reduce carga en el servidor Strapi
  • TTL configurable (default: 1 hora)
  • Invalidación manual vía POST /api/menus
5

Endpoint Personalizado - Places

// El BFF combina múltiples colecciones de Strapi automáticamente
const response = await fetch('/api/places?postType=place&debug=true');
const { data, meta } = await response.json();

// Cada place incluye:
// - Datos del post (título, contenido, fecha)
// - Términos asociados (categorías/tags)
// - Metadatos (dirección, teléfono)
// - Imágenes (portada + galería)
console.log(data[0]);
/*
{
  documentId: "abc123",
  title: "Mi Lugar",
  address: "Calle Principal 123",
  phone: "+52 123 456 7890",
  featuredImageUrl: "https://...",
  galleryUrls: ["https://...", ...],
  terms: [{ name: "Restaurante", slug: "restaurante" }],
  ...
}
*/
6

Autenticación - Login

const response = await fetch('/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    identifier: 'user@example.com', // o username
    password: 'password123'
  })
});

const { jwt, user } = await response.json();
// Guardar el token para usar en peticiones futuras
localStorage.setItem('token', jwt);
7

Registro de Usuario

const response = await fetch('/api/auth/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'johndoe',
    email: 'john@example.com',
    password: 'password123'
  })
});

const { jwt, user } = await response.json();
localStorage.setItem('token', jwt);
8

Peticiones Autenticadas

// Obtener datos del usuario actual
const token = localStorage.getItem('token');

const response = await fetch('/api/user/me', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

const { data: userData } = await response.json();
console.log(userData.username, userData.email);
9

Recuperar Contraseña

Paso 1: Solicitar código

await fetch('/api/auth/forgot-password', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'user@example.com'
  })
});
// El usuario recibirá un email con el código

Paso 2: Restablecer con código

await fetch('/api/auth/reset-password', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    code: 'codigo_del_email',
    password: 'nuevaPassword123',
    passwordConfirmation: 'nuevaPassword123'
  })
});

Endpoints del BFF

🔐 Autenticación

POST/api/auth/login
POST/api/auth/register
GET/api/auth/verify
POST/api/auth/forgot-password
POST/api/auth/reset-password
POST/api/auth/change-password

👤 Usuario

GET/api/user/me
PUT/api/user/update

🎯 Endpoints Personalizados

GET/api/users→ Usuarios con metadata
GET/api/places→ Listar lugares
GET/api/places/[documentId]→ Obtener lugar
GET/api/events→ Eventos con cache
GET/api/events/[id]→ Evento específico
GET/api/menus→ Menús con Redis
GET/api/menus/[name]→ Menú específico
GET/api/recent→ Posts recientes con Redis

📡 Proxy Strapi

GET/api/strapi/[tu-content-type]
POST/api/strapi/[tu-content-type]
PUT/api/strapi/[tu-content-type]/[id]
DELETE/api/strapi/[tu-content-type]/[id]

Próximos Pasos

  1. Configura tu backend en .env.local
  2. Si usas Strapi: crea content types y configura permisos
  3. Adapta las rutas de los ejemplos según tu API
  4. Implementa login/registro en tu frontend
  5. Prueba los endpoints con el health check