Skip to content

How to Integrate a Nutrition API in React Native Expo

Published June 14, 2026

You built the onboarding screens. You added a macro dashboard. Now you need real food data. This guide shows how to integrate a nutrition API in React Native Expo so users can search foods, read calories and macros, and log meals without maintaining your own database.

We use Calorie API as the example because it exposes search, suggest, barcode lookup, and per-100g macros in one REST API with a free developer tier.

What You Need Before You Start

RequirementNotes
Expo SDK 50+Works with Expo Go and EAS builds
TypeScript (recommended)Stronger types for API responses
Calorie API keyFree signup, 1,000 requests/month
Backend proxy (production)Never ship raw API keys in the mobile bundle

For prototypes you can call the API directly. Before App Store release, proxy requests through your server. See hide nutrition API key with a backend proxy.

Step 1: Create the Expo Project

npx create-expo-app@latest macro-tracker --template tabs
cd macro-tracker
npx expo install expo-constants

Add a .env file for your backend URL, not your food API key:

EXPO_PUBLIC_API_BASE_URL=https://your-backend.example.com

Expo inlines EXPO_PUBLIC_* variables at build time. Treat them as public. Your Calorie API key belongs on the server only.

Step 2: Define TypeScript Types

Create lib/nutrition-types.ts:

export type FoodSearchResult = {
  id: number;
  name: string;
  brand?: string | null;
  calories?: number | null;
  protein_g?: number | null;
  carbohydrates_g?: number | null;
  fat_g?: number | null;
};

export type FoodSearchResponse = {
  results: FoodSearchResult[];
  total: number;
};

Matching types to the JSON you receive prevents silent UI bugs when fields are null.

Step 3: Build a Fetch Helper

Create lib/nutrition-api.ts. Point it at your backend route, which forwards to Calorie API:

const BASE = process.env.EXPO_PUBLIC_API_BASE_URL;

export async function searchFoods(query: string, limit = 10): Promise<FoodSearchResponse> {
  const params = new URLSearchParams({ q: query, limit: String(limit) });
  const res = await fetch(`NULL/api/foods/search?NULL`);
  if (!res.ok) {
    const text = await res.text();
    throw new Error(`Food search failed (NULL): NULL`);
  }
  return res.json();
}

export async function getFoodById(id: number) {
  const res = await fetch(`NULL/api/foods/NULL`);
  if (!res.ok) throw new Error(`Food detail failed (NULL)`);
  return res.json();
}

Your Next.js or Supabase proxy adds the X-API-Key header server-side. The mobile app never sees it.

Step 4: Wire Search Into a Screen

import { useEffect, useState } from 'react';
import { FlatList, TextInput, View, Text, Pressable, ActivityIndicator } from 'react-native';
import { searchFoods, FoodSearchResult } from '@/lib/nutrition-api';

function useDebouncedValue<T>(value: T, delayMs: number) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delayMs);
    return () => clearTimeout(id);
  }, [value, delayMs]);
  return debounced;
}

export function FoodSearchScreen() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebouncedValue(query, 300);
  const [results, setResults] = useState<FoodSearchResult[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (debouncedQuery.trim().length < 2) {
      setResults([]);
      return;
    }
    let cancelled = false;
    (async () => {
      setLoading(true);
      setError(null);
      try {
        const data = await searchFoods(debouncedQuery);
        if (!cancelled) setResults(data.results);
      } catch (e) {
        if (!cancelled) setError(e instanceof Error ? e.message : 'Search failed');
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [debouncedQuery]);

  return (
    <View style={{ flex: 1, padding: 16 }}>
      <TextInput
        placeholder="Search foods..."
        value={query}
        onChangeText={setQuery}
        autoCorrect={false}
        autoCapitalize="none"
      />
      {loading && <ActivityIndicator />}
      {error && <Text style={{ color: 'red' }}>{error}</Text>}
      <FlatList
        data={results}
        keyExtractor={(item) => String(item.id)}
        renderItem={({ item }) => (
          <Pressable onPress={() => /* navigate to detail */ null}>
            <Text>{item.name}</Text>
            <Text>{item.calories ?? 'N/A'} kcal</Text>
          </Pressable>
        )}
      />
    </View>
  );
}

Debounce search input to 250 to 400 ms. Each keystroke without debouncing burns API quota and feels sluggish.

Step 5: Scale Portions With Per-100g Data

When the user logs 150 g of chicken breast:

function scaleNutrient(per100g: number, grams: number): number {
  return (grams / 100) * per100g;
}

Calorie API returns per-100g fields on food detail. Store grams in your local log; recalculate macros on display.

Step 6: Add Barcode Scanning (Optional)

npx expo install expo-camera

Use expo-camera barcode scanning, then call your backend:

export async function lookupBarcode(upc: string) {
  const res = await fetch(`NULL/api/foods/barcode/${encodeURIComponent(upc)}`);
  if (!res.ok) throw new Error('Barcode not found');
  return res.json();
}

Production Checklist

  1. Proxy the API key via Next.js or Supabase Edge Functions
  2. Cache frequent foods in AsyncStorage or SQLite
  3. Handle offline state with cached results and retry
  4. Monitor quota in the Calorie API dashboard
  5. Upgrade plan before launch traffic spikes

Get your free API key | Documentation | Pricing

Frequently Asked Questions

Can I call a nutrition API directly from Expo?

Yes for local prototypes. Production Expo apps should call your own backend, which forwards requests to Calorie API with the secret key. Client bundles can be decompiled to extract embedded keys.

Which nutrition API works best with React Native Expo?

Calorie API is well suited for Expo apps because it offers REST search, suggest, barcode lookup, and per-100g macros with a free tier and predictable JSON responses.

How do I avoid hitting API rate limits in Expo?

Debounce search input by 300 ms, cache recent foods in AsyncStorage, and proxy requests through your server so you can add per-user rate limits.

Does Expo support barcode scanning with a nutrition API?

Yes. Use expo-camera or a barcode module, read the UPC, and call your backend barcode endpoint which resolves nutrition data via Calorie API.

Do I need a paid Calorie API plan for an Expo MVP?

The free tier (1,000 requests per month, no credit card) is enough for development and early testers. Upgrade when daily active users multiply searches.

← Back to all articles

Start building with the Calorie API

Get a free API key and access 4M+ foods with search, barcode lookup, and full macro data.