Overview

This guide shows you how to fetch menu data from the Crave API and display it in your storefront. The menu endpoint provides structured data including categories, products, pricing, and modifiers. The Crave API returns menu data in a hierarchical structure:
{
  "categories": [
    {
      "id": "cat_123",
      "name": "Appetizers",
      "description": "Start your meal right",
      "display_order": 1,
      "is_active": true,
      "products": [
        {
          "id": "prod_456",
          "name": "Buffalo Wings",
          "description": "Crispy wings with buffalo sauce",
          "price": 1200,
          "image_url": "https://...",
          "is_available": true,
          "modifiers": [
            {
              "id": "mod_789",
              "name": "Spice Level",
              "type": "radio",
              "required": true,
              "options": [
                {
                  "id": "opt_101",
                  "name": "Mild",
                  "price": 0
                },
                {
                  "id": "opt_102", 
                  "name": "Hot",
                  "price": 0
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Fetching Menu Data

Basic Menu Fetch

async function fetchMenu(locationId) {
  try {
    const response = await fetch(`https://api.cravejs.com/api/v1/locations/${locationId}/menus`, {
      headers: {
        'X-API-Key': process.env.CRAVE_API_KEY,
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const menu = await response.json();
    return menu;
  } catch (error) {
    console.error('Failed to fetch menu:', error);
    throw error;
  }
}

Server-Side API Route (Next.js)

// pages/api/menu.js
export default async function handler(req, res) {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { locationId } = req.query;

  if (!locationId) {
    return res.status(400).json({ error: 'Location ID is required' });
  }

  try {
    const response = await fetch(`https://api.cravejs.com/api/v1/locations/${locationId}/menus`, {
      headers: {
        'X-API-Key': process.env.CRAVE_API_KEY,
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`API request failed: ${response.status}`);
    }

    const menu = await response.json();
    res.json(menu);
  } catch (error) {
    console.error('Menu fetch error:', error);
    res.status(500).json({ error: 'Failed to fetch menu' });
  }
}

Displaying Menu Data

Basic Menu Component

import { useState, useEffect } from 'react';

export default function Menu({ locationId }) {
  const [menu, setMenu] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function loadMenu() {
      try {
        setLoading(true);
        const response = await fetch(`/api/menu?locationId=${locationId}`);
        
        if (!response.ok) {
          throw new Error('Failed to load menu');
        }
        
        const data = await response.json();
        setMenu(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    if (locationId) {
      loadMenu();
    }
  }, [locationId]);

  if (loading) return <div>Loading menu...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!menu) return <div>No menu available</div>;

  return (
    <div className="menu">
      <h2>Menu</h2>
      {menu.categories?.map(category => (
        <CategorySection key={category.id} category={category} />
      ))}
    </div>
  );
}

function CategorySection({ category }) {
  return (
    <div className="category">
      <h3>{category.name}</h3>
      {category.description && (
        <p className="category-description">{category.description}</p>
      )}
      
      <div className="products">
        {category.products?.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

function ProductCard({ product }) {
  return (
    <div className="product-card">
      {product.image_url && (
        <img src={product.image_url} alt={product.name} />
      )}
      
      <div className="product-info">
        <h4>{product.name}</h4>
        <p className="description">{product.description}</p>
        <p className="price">${(product.price / 100).toFixed(2)}</p>
        
        {!product.is_available && (
          <p className="unavailable">Currently unavailable</p>
        )}
        
        {product.modifiers && product.modifiers.length > 0 && (
          <div className="modifiers">
            {product.modifiers.map(modifier => (
              <ModifierGroup key={modifier.id} modifier={modifier} />
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

function ModifierGroup({ modifier }) {
  return (
    <div className="modifier-group">
      <h5>{modifier.name}</h5>
      {modifier.required && <span className="required">*</span>}
      
      <div className="modifier-options">
        {modifier.options?.map(option => (
          <label key={option.id} className="modifier-option">
            <input
              type={modifier.type === 'radio' ? 'radio' : 'checkbox'}
              name={modifier.id}
              value={option.id}
            />
            <span>{option.name}</span>
            {option.price > 0 && (
              <span className="option-price">
                +${(option.price / 100).toFixed(2)}
              </span>
            )}
          </label>
        ))}
      </div>
    </div>
  );
}

Advanced Menu Features

function useMenuFilters(menu) {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState('');

  const filteredMenu = useMemo(() => {
    if (!menu) return null;

    return {
      ...menu,
      categories: menu.categories
        .filter(category => 
          !selectedCategory || category.id === selectedCategory
        )
        .map(category => ({
          ...category,
          products: category.products.filter(product =>
            product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
            product.description.toLowerCase().includes(searchTerm.toLowerCase())
          )
        }))
        .filter(category => category.products.length > 0)
    };
  }, [menu, searchTerm, selectedCategory]);

  return {
    filteredMenu,
    searchTerm,
    setSearchTerm,
    selectedCategory,
    setSelectedCategory
  };
}
const menuCache = new Map();

async function getCachedMenu(locationId) {
  const cacheKey = `menu_${locationId}`;
  
  // Check cache first
  if (menuCache.has(cacheKey)) {
    const cached = menuCache.get(cacheKey);
    
    // Check if cache is still valid (5 minutes)
    if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
      return cached.data;
    }
  }
  
  // Fetch fresh data
  const menu = await fetchMenu(locationId);
  
  // Cache the result
  menuCache.set(cacheKey, {
    data: menu,
    timestamp: Date.now()
  });
  
  return menu;
}

Error Handling

Common Menu Errors

function handleMenuError(error) {
  switch (error.status) {
    case 401:
      return 'Invalid API key. Please check your configuration.';
    case 403:
      return 'Access forbidden. Merchant subscription may be required.';
    case 404:
      return 'Menu not found. Please check the location ID.';
    case 429:
      return 'Too many requests. Please wait before trying again.';
    default:
      return 'Failed to load menu. Please try again.';
  }
}

Loading States

function MenuWithStates({ locationId }) {
  const [menu, setMenu] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const loadMenu = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const data = await fetchMenu(locationId);
      setMenu(data);
    } catch (err) {
      setError(handleMenuError(err));
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return (
      <div className="menu-loading">
        <div className="spinner"></div>
        <p>Loading menu...</p>
      </div>
    );
  }

  if (error) {
    return (
      <div className="menu-error">
        <p>{error}</p>
        <button onClick={loadMenu}>Retry</button>
      </div>
    );
  }

  return <Menu menu={menu} />;
}

Styling Examples

Basic CSS

.menu {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.category {
  margin-bottom: 3rem;
}

.category h3 {
  font-size: 1.5rem;
  margin-bottom: 1rem;
  color: #333;
}

.products {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1.5rem;
}

.product-card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 1rem;
  background: white;
}

.product-card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 4px;
  margin-bottom: 1rem;
}

.price {
  font-size: 1.2rem;
  font-weight: bold;
  color: #007bff;
}

.unavailable {
  color: #dc3545;
  font-style: italic;
}

.modifier-group {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid #eee;
}

.modifier-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.option-price {
  margin-left: auto;
  color: #666;
}

Best Practices

  1. Caching: Cache menu data to reduce API calls
  2. Error Handling: Provide clear error messages and retry options
  3. Loading States: Show loading indicators while fetching data
  4. Responsive Design: Ensure menu works on all device sizes
  5. Accessibility: Use proper semantic HTML and ARIA labels
  6. Performance: Lazy load images and consider pagination for large menus

Next Steps