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.Menu Data Structure
The Crave API returns menu data in a hierarchical structure:Copy
{
"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
Copy
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)
Copy
// 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
Copy
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
Filtering and Search
Copy
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
};
}
Menu Caching
Copy
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
Copy
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
Copy
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
Copy
.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
- Caching: Cache menu data to reduce API calls
- Error Handling: Provide clear error messages and retry options
- Loading States: Show loading indicators while fetching data
- Responsive Design: Ensure menu works on all device sizes
- Accessibility: Use proper semantic HTML and ARIA labels
- Performance: Lazy load images and consider pagination for large menus
Next Steps
- Processing Orders - Handle order creation
- Implementing Checkout - Add payment processing
- API Reference - Detailed endpoint documentation