Skip to main content
Recommendations for building great personalized menu experiences with EveryBite.

Performance

Prefetch Filter Options

Load filter options when the menu screen initializes, not when the user opens the filter panel:
// Good: Prefetch on screen load (uses X-Session-ID header for context)
useEffect(() => {
  prefetchFilterOptions();
}, [sessionId]);

// Bad: Fetch when filter panel opens
const onFilterPress = async () => {
  const filters = await fetchFilterOptions(); // Causes delay
  showFilterPanel(filters);
};

Use Pagination

Never fetch all dishes at once. Use cursor-based pagination:
query SearchDishes($pagination: PaginationInput) {
  search(pagination: $pagination) {
    matches {
      dish { id name }
      matchStatus
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
Variables:
{
  "pagination": { "first": 20, "after": "cursor_abc123" }
}
Chain context comes from your session via the X-Session-ID header. You don’t need to pass chainId in the query.

Optimize Field Selection

Only request fields you need. GraphQL lets you be specific:
# Good: Request only needed fields
query {
  search(...) {
    matches {
      dish {
        id
        name
        imageUrl
        nutrition { calories }
      }
    }
  }
}

# Bad: Request everything
query {
  search(...) {
    matches {
      dish {
        id
        name
        description
        imageUrl
        nutrition { ...allFields }
        allergens { ...allFields }
        ingredients { ...allFields }
      }
    }
  }
}

User Experience

Display Match Status Clearly

Use visual hierarchy to communicate match status:
StatusDisplayColor
MATCHShow prominentlyGreen accent
ALMOST_MATCHShow with warningYellow/Orange
NOT_MATCHDe-emphasize or hideGray/Muted

Show Match Reasons

When a dish is an “Almost Match”, explain why:
{almostMatches.map(({ dish, matchReasons }) => (
  <DishCard
    dish={dish}
    warnings={matchReasons} // ["Contains Dairy", "Above calorie limit"]
  />
))}

Persist Preferences

Save user preferences locally for returning users:
// Save preferences after user sets them
localStorage.setItem('guestPreferences', JSON.stringify(preferences));

// Restore on app launch
const savedPrefs = localStorage.getItem('guestPreferences');
if (savedPrefs) {
  applyPreferences(JSON.parse(savedPrefs));
}

Security

Protect API Keys

Never expose API keys in client-side code. Always proxy requests through your backend.
// Bad: API key in client
fetch('https://api.everybite.com/smartmenu/graphql', {
  headers: { 'Authorization': 'pk_secret_key' } // Exposed!
});

// Good: Proxy through your backend
fetch('/api/menu/search', { body: preferences });
// Your backend adds the API key server-side

Validate Session IDs

Generate session IDs server-side or use cryptographically secure methods:
// Good: Secure session ID
const sessionId = `sess_${crypto.randomUUID()}`;

// Bad: Predictable session ID
const sessionId = `sess_${userId}_${Date.now()}`; // Guessable

Passing IDs Securely

All communication with the SmartMenu API happens over HTTPS, which means every request—including the startSession mutation and X-Session-ID header—is encrypted in transit. No one can intercept or read these values as they travel between your servers and ours. IDs like guestId and passportId are passed once when you call startSession. After that, only the X-Session-ID header is needed on subsequent calls. Follow these best practices for the IDs you send: Use opaque identifiers Use random, meaningless strings rather than sequential numbers that could be guessed:
// Good: Random, unpredictable
guestId: 'usr_7Kx9mP2qL4nR8vT1'

// Avoid: Sequential, guessable
guestId: 'user_123'
Never use personal information as IDs Don’t use email addresses, phone numbers, or other personal data as identifiers:
// Good: Opaque reference
guestId: 'lyl_8Hj2kM5pQ9'

// Avoid: Exposes personal information
guestId: 'john.doe@email.com'
Hash IDs if you prefer not to share them If you’d rather not send your internal customer IDs to EveryBite, you can hash them before sending. Use a consistent salt so you can still correlate analytics data later:
// Hash your internal ID before sending
const hashedId = sha256(internalCustomerId + YOUR_SECRET_SALT);
const guestId = hashedId;

// You can always recreate the same hash to correlate data
As long as you’re proxying API calls through your backend (as recommended above), these IDs only travel server-to-server and are never exposed to end users.

Common Pitfalls

Always have UI for when no dishes match the user’s preferences. Suggest loosening filters.
Almost Matches are valuable - they show dishes that are close. Don’t hide them completely.
Search results are personalized per user. Don’t cache them across users.
Handle network errors, rate limits, and API errors gracefully with retry logic.