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
| Requirement | Notes |
|---|---|
| Expo SDK 50+ | Works with Expo Go and EAS builds |
| TypeScript (recommended) | Stronger types for API responses |
| Calorie API key | Free 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
- Proxy the API key via Next.js or Supabase Edge Functions
- Cache frequent foods in AsyncStorage or SQLite
- Handle offline state with cached results and retry
- Monitor quota in the Calorie API dashboard
- Upgrade plan before launch traffic spikes
Related Guides
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.
