Skip to main content

Best Practices

Follow these patterns to build robust, performant integrations with the EveryBite APIs.

Caching

Client-Side Caching

Menu data changes infrequently. Cache aggressively.
// Apollo Client with smart caching
const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Dish: {
        // Cache dishes by ID across queries
        keyFields: ['id'],
      },
      Query: {
        fields: {
          dishes: {
            // Merge paginated results
            keyArgs: ['menuKey', 'filters'],
            merge(existing, incoming) {
              return {
                ...incoming,
                results: [...(existing?.results || []), ...incoming.results]
              };
            }
          }
        }
      }
    }
  }),
  defaultOptions: {
    watchQuery: {
      // Return cached data while fetching fresh
      fetchPolicy: 'cache-and-network'
    }
  }
});

Cache Durations

Data TypeRecommended TTLWhy
Menu structure1 hourCategories rarely change
Dish details15 minutesPrices/availability may change
Nutrition data24 hoursVery stable
Filter options1 hourBased on menu
Search suggestions5 minutesUser-specific context

Server-Side Caching

For backend integrations:
// Redis caching example
async function getDishes(menuKey, filters) {
  const cacheKey = `dishes:${menuKey}:${hash(filters)}`;

  // Try cache first
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // Fetch from API
  const data = await everyBiteClient.query({
    query: GET_DISHES,
    variables: { menuKey, filters }
  });

  // Cache for 15 minutes
  await redis.setex(cacheKey, 900, JSON.stringify(data));

  return data;
}

Error Handling

GraphQL Errors

EveryBite returns structured errors:
{
  "errors": [
    {
      "message": "Invalid menu key",
      "extensions": {
        "code": "INVALID_MENU_KEY",
        "menuKey": "mk_invalid"
      }
    }
  ]
}

Error Codes

CodeMeaningAction
INVALID_MENU_KEYKey doesn’t exist or expiredCheck dashboard for valid key
UNAUTHORIZED_BRANDKey doesn’t have accessContact support for access
INVALID_FILTERMalformed filter parameterValidate input
DISH_NOT_FOUNDDish ID doesn’t existMay have been removed
RATE_LIMITEDToo many requestsImplement backoff
INVALID_TOKENPassport token invalidRe-authenticate user

Handling Pattern

function useMenuWithErrorHandling(menuKey) {
  const { data, error, loading } = useQuery(GET_DISHES, {
    variables: { menuKey }
  });

  // Handle specific error types
  if (error) {
    const code = error.graphQLErrors?.[0]?.extensions?.code;

    switch (code) {
      case 'INVALID_MENU_KEY':
        return { error: 'Configuration error. Please contact support.' };
      case 'RATE_LIMITED':
        return { error: 'Too many requests. Please wait a moment.' };
      case 'UNAUTHORIZED_BRAND':
        return { error: 'Access not configured for this restaurant.' };
      default:
        return { error: 'Unable to load menu. Please try again.' };
    }
  }

  return { data, loading };
}

Rate Limiting

Limits by Key Type

Key TypeRequests/minRequests/day
Restaurant Key6010,000
Chain Key300100,000
Brand Key1,000Unlimited

Reading Rate Limit Headers

// Response headers
const headers = response.headers;
const limit = headers.get('X-RateLimit-Limit');
const remaining = headers.get('X-RateLimit-Remaining');
const reset = headers.get('X-RateLimit-Reset'); // Unix timestamp

if (remaining < 10) {
  console.warn(`Rate limit warning: ${remaining} requests remaining`);
}

Implementing Backoff

async function queryWithBackoff(query, variables, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.query({ query, variables });
    } catch (error) {
      const isRateLimited =
        error.graphQLErrors?.[0]?.extensions?.code === 'RATE_LIMITED';

      if (isRateLimited && attempt < maxRetries - 1) {
        // Exponential backoff: 1s, 2s, 4s
        const delay = Math.pow(2, attempt) * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      throw error;
    }
  }
}

Security

Protect Your Menu Key

Never expose your Menu Key in client-side code if you have a backend.
For apps with a backend:
// Backend proxy
app.post('/api/menu', async (req, res) => {
  const data = await everyBiteClient.query({
    query: GET_DISHES,
    variables: {
      menuKey: process.env.EVERYBITE_MENU_KEY, // Server-side only
      filters: req.body.filters
    }
  });
  res.json(data);
});
For client-only apps: Restaurant Keys are designed to be safe for client exposure since they only provide read access to public menu data. However:
  • Use environment variables, not hardcoded strings
  • Configure domain restrictions in your dashboard
  • Monitor usage for anomalies

Passport Token Handling

// Store tokens securely
function storePassportToken(token) {
  // Use httpOnly cookie for web
  document.cookie = `passport_token=${token}; Secure; SameSite=Strict`;

  // Or secure storage for mobile
  SecureStore.setItemAsync('passport_token', token);
}

// Never log tokens
console.log('User logged in'); // Good
console.log(`Token: ${token}`); // Bad!

Validate User Input

// Sanitize search queries
function sanitizeSearch(query) {
  return query
    .trim()
    .substring(0, 100) // Limit length
    .replace(/[<>]/g, ''); // Remove potential XSS
}

// Validate filter values
function validateFilters(filters) {
  const validDiets = ['VEGAN', 'VEGETARIAN', 'GLUTEN_FREE', ...];

  if (filters.diets) {
    filters.diets = filters.diets.filter(d => validDiets.includes(d));
  }

  if (filters.caloriesMax) {
    filters.caloriesMax = Math.min(Math.max(0, filters.caloriesMax), 5000);
  }

  return filters;
}

Performance

Query Only What You Need

# Bad: Fetching everything
query {
  dishes(menuKey: "...") {
    results {
      dish {
        id name description imageUrl price
        nutrition { calories protein carbohydrates fatTotal fatSaturated ... }
        allergens { type displayName severity }
        customizationGroups { ... }
      }
    }
  }
}

# Good: Fetch for list view
query DishList {
  dishes(menuKey: "...") {
    results {
      dish {
        id name imageUrl price
        nutrition { calories }
      }
      matchStatus
    }
  }
}

# Good: Fetch details on demand
query DishDetail($id: ID!) {
  dish(menuKey: "...", dishId: $id) {
    dish {
      id name description imageUrl price
      nutrition { calories protein carbohydrates fatTotal }
      allergens { type displayName }
      customizationGroups { ... }
    }
  }
}

Pagination

Always paginate large result sets:
query PaginatedDishes($after: String) {
  dishes(
    menuKey: "..."
    pagination: { first: 20, after: $after }
  ) {
    results { ... }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}

Prefetching

Anticipate user actions:
function DishCard({ dish }) {
  const client = useApolloClient();

  // Prefetch details on hover
  const handleMouseEnter = () => {
    client.query({
      query: GET_DISH_DETAILS,
      variables: { dishId: dish.id }
    });
  };

  return (
    <div onMouseEnter={handleMouseEnter}>
      {/* ... */}
    </div>
  );
}

Testing

Use the Sandbox

Always develop against sandbox:
const API_URL = process.env.NODE_ENV === 'production'
  ? 'https://api.everybite.com/graphql'
  : 'https://api.everybite-stage.com/graphql';

Mock Data for Unit Tests

// __mocks__/dishes.js
export const mockDishes = {
  results: [
    {
      dish: {
        id: 'dish_1',
        name: 'Test Salad',
        nutrition: { calories: 350 },
        allergens: []
      },
      matchStatus: 'MATCH'
    }
  ],
  totalCount: 1
};

// Component test
test('renders dishes', () => {
  render(
    <MockedProvider mocks={[{
      request: { query: GET_DISHES, variables: { menuKey: 'test' } },
      result: { data: { dishes: mockDishes } }
    }]}>
      <Menu />
    </MockedProvider>
  );

  expect(screen.getByText('Test Salad')).toBeInTheDocument();
});

Monitoring

Track these metrics:
MetricWhy
API response timeDetect latency issues
Error rate by codeIdentify recurring problems
Cache hit rateOptimize caching strategy
Rate limit proximityPrevent hitting limits
// Example: Track with your analytics
client.query({ query, variables })
  .then(result => {
    analytics.track('api_call', {
      query: query.definitions[0].name.value,
      duration: Date.now() - startTime,
      cached: result.loading === false
    });
  })
  .catch(error => {
    analytics.track('api_error', {
      code: error.graphQLErrors?.[0]?.extensions?.code,
      query: query.definitions[0].name.value
    });
  });