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 con post_meta JSON) 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

Reviews y Ratings

Obtén reviews y calificaciones de los lugares. Los reviews se extraen de la colección wx-reviews de Strapi.

Incluir resumen de ratings:

// Obtener lugar con resumen de calificaciones
const response = await fetch(
  '/api/places/la-chosita-del-marisco?includeRatings=true'
);
const { data } = await response.json();

console.log(data.rating);
/*
{
  average: 3.7,
  total: 3,
  distribution: {
    5: 1,
    4: 1,
    3: 0,
    2: 1,
    1: 0
  }
}
*/

Incluir reviews completas:

// Obtener lugar con todas las reviews
const response = await fetch(
  '/api/places/la-chosita-del-marisco?includeReviews=true'
);
const { data } = await response.json();

console.log(data.reviews);
/*
[
  {
    id: 16,
    documentId: "wn0e8tvzl8urat2oh1qll6wy",
    title: "Excelente servicio",
    content: "La comida estuvo deliciosa...",
    rating: 5,
    source: "web",
    date: "2026-02-25T00:44:19.107Z",
    approved: true,
    likes: 2,
    dislikes: 0,
    author: {
      id: 4,
      username: "usuario123",
      avatar: "https://..."
    }
  },
  ...
]
*/

Combinar ratings + reviews:

// Obtener lugar completo con ratings y reviews
const response = await fetch(
  '/api/places/la-chosita-del-marisco?includeRatings=true&includeReviews=true'
);
const { data } = await response.json();

// data ahora incluye:
// - rating: { average, total, distribution }
// - reviews: [{ id, title, content, rating, ... }]

Componente React con ratings:

function PlaceWithRatings({ documentId }) {
  const [place, setPlace] = useState(null);

  useEffect(() => {
    fetch(`/api/places/${documentId}?includeRatings=true&includeReviews=true`)
      .then(res => res.json())
      .then(({ data }) => setPlace(data));
  }, [documentId]);

  if (!place) return <div>Cargando...</div>;

  return (
    <div>
      <h1>{place.title}</h1>
      
      {/* Rating Summary */}
      {place.rating && (
        <div className="flex items-center gap-2">
          <span className="text-2xl font-bold">{place.rating.average}</span>
          <div className="flex">
            {[1,2,3,4,5].map(star => (
              <span key={star} className={
                star <= Math.round(place.rating.average)
                  ? 'text-yellow-400'
                  : 'text-gray-300'
              }>★</span>
            ))}
          </div>
          <span className="text-gray-500">
            ({place.rating.total} reviews)
          </span>
        </div>
      )}

      {/* Distribution */}
      {place.rating && (
        <div className="mt-4">
          {[5,4,3,2,1].map(stars => (
            <div key={stars} className="flex items-center gap-2">
              <span>{stars}★</span>
              <div className="flex-1 bg-gray-200 rounded h-2">
                <div 
                  className="bg-yellow-400 h-2 rounded"
                  style={{ 
                    width: `${(place.rating.distribution[stars] / place.rating.total) * 100}%` 
                  }}
                />
              </div>
              <span>{place.rating.distribution[stars]}</span>
            </div>
          ))}
        </div>
      )}

      {/* Reviews List */}
      {place.reviews?.map(review => (
        <div key={review.documentId} className="border-t pt-4 mt-4">
          <div className="flex items-center gap-2">
            <span className="font-bold">{review.author?.username || 'Anónimo'}</span>
            <span className="text-yellow-400">{review.rating}★</span>
          </div>
          <h3 className="font-semibold">{review.title}</h3>
          <p className="text-gray-600">{review.content}</p>
          <div className="flex gap-4 text-sm text-gray-500">
            <span>👍 {review.likes}</span>
            <span>👎 {review.dislikes}</span>
          </div>
        </div>
      ))}
    </div>
  );
}
📊 Estructura de Rating:
  • average: Promedio de calificaciones (0-5, 1 decimal)
  • total: Total de reviews aprobadas
  • distribution: Cantidad por estrella (1-5)

✅ 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

✏️CRUD de Menús - Gestión desde el Frontend

Ahora puedes crear, actualizar y eliminar items de menú directamente desde tu frontend. El cache de Redis se invalida automáticamente tras cada operación.

POSTCrear nuevo item de menú:

// Crear un nuevo item en el menú "desktop-header"
const response = await fetch('/api/menus/desktop-header/items', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': process.env.NEXT_PUBLIC_BFF_API_KEY,
    'Authorization': 'Bearer ' + userToken
  },
  body: JSON.stringify({
    post: {
      post_title: 'Contacto',
      post_name: 'contacto',
      menu_order: 5
    },
    metas: {
      _menu_item_url: '/contacto',
      _menu_item_type: 'custom',
      _menu_item_target: '_self',
      _menu_item_classes: 'btn btn-primary'
    }
  })
});

const { success, data, meta } = await response.json();
// data: menú actualizado con el nuevo item
// meta.itemDocumentId: documentId del item creado

PUTActualizar item existente:

// Actualizar un item del menú "desktop-header"
const response = await fetch('/api/menus/desktop-header', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': process.env.NEXT_PUBLIC_BFF_API_KEY,
    'Authorization': 'Bearer ' + userToken
  },
  body: JSON.stringify({
    itemDocumentId: 'abc123xyz', // documentId del item a actualizar
    post: {
      post_title: 'Nuevo Título',
      menu_order: 10
    },
    metas: {
      _menu_item_url: '/nueva-url',
      _menu_item_target: '_blank'
    }
  })
});

const { success, data } = await response.json();
// data: menú actualizado
// Cache de Redis invalidado automáticamente

DELETEEliminar item de menú:

// Eliminar un item del menú
const response = await fetch('/api/menus/desktop-header', {
  method: 'DELETE',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': process.env.NEXT_PUBLIC_BFF_API_KEY,
    'Authorization': 'Bearer ' + userToken
  },
  body: JSON.stringify({
    itemDocumentId: 'abc123xyz'
  })
});

const { success, data, meta } = await response.json();
// data: menú actualizado sin el item eliminado
// meta.deleted: true

Ejemplo completo: Componente de gestión de menús

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

export function MenuEditor({ menuName, userToken }) {
  const [menu, setMenu] = useState(null);
  const [loading, setLoading] = useState(false);

  async function handleCreateItem(itemData) {
    setLoading(true);
    const res = await fetch(`/api/menus/${menuName}/items`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.NEXT_PUBLIC_BFF_API_KEY,
        'Authorization': `Bearer ${userToken}`
      },
      body: JSON.stringify(itemData)
    });
    
    const { success, data } = await res.json();
    if (success) {
      setMenu(data); // Menú actualizado
      alert('✅ Item creado exitosamente');
    }
    setLoading(false);
  }

  async function handleUpdateItem(itemDocId, updates) {
    const res = await fetch(`/api/menus/${menuName}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.NEXT_PUBLIC_BFF_API_KEY,
        'Authorization': `Bearer ${userToken}`
      },
      body: JSON.stringify({ itemDocumentId: itemDocId, ...updates })
    });
    
    const { success, data } = await res.json();
    if (success) setMenu(data);
  }

  async function handleDeleteItem(itemDocId) {
    const confirmed = confirm('¿Eliminar este item?');
    if (!confirmed) return;

    const res = await fetch(`/api/menus/${menuName}`, {
      method: 'DELETE',
      headers: {
        'Content-Type': 'application/json',
        'X-API-Key': process.env.NEXT_PUBLIC_BFF_API_KEY,
        'Authorization': `Bearer ${userToken}`
      },
      body: JSON.stringify({ itemDocumentId: itemDocId })
    });
    
    const { success, data } = await res.json();
    if (success) {
      setMenu(data);
      alert('🗑️ Item eliminado');
    }
  }

  return (
    <div>
      <button onClick={() => handleCreateItem({ 
        post: { post_title: 'Nuevo Item' },
        metas: { _menu_item_url: '/nueva-pagina' }
      })}>
        Crear Item
      </button>
      {/* Renderizar items con botones de editar/eliminar */}
    </div>
  );
}
🔐 Requisitos de Autenticación:
  • API Key: Header X-API-Key
  • Token JWT: Header Authorization: Bearer ...
  • Cache: Se invalida automáticamente tras cada operación
  • Respuesta: Incluye el menú completo actualizado
📋Metadatos Comunes de Items de Menú:
_menu_item_url

URL del enlace

_menu_item_type

custom, post_type, taxonomy

_menu_item_target

_blank, _self

_menu_item_classes

Clases CSS

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