Recipe Lookup with TheMealDB
Food Tracker (React + Express)
Chapter 9 · Recipe Lookup with TheMealDB
Chapter 6's alerts query already knows what's expiring soon. This chapter takes that same list and asks a second free API, TheMealDB, what could actually be cooked with it.
Extending the Schema: A Recipe Cache
The same caching pattern Chapter 3 established for barcode lookups, applied to per-ingredient recipe results:
The Suggestion Route
A meal matching three expiring ingredients ranks above one matching only one — the same relevance-by-match-count sorting every course in this quartet uses for its own recipe feature.
Promise.all fires every ingredient's lookup concurrently instead of one after another, because Node's own async model makes that the natural way to write it, not a special optimization bolted on afterward. Five expiring ingredients means five requests in flight at once here, rather than five requests run one after the other — a genuine payoff of Chapter 1's own "one language, both ends" framing, where JavaScript's async-first design turns out to matter for more than just tooling convenience.
filter.php?i= expects TheMealDB's own specific ingredient vocabulary (chicken_breast, not chicken or chicken breasts) — a real, generic pantry item name like "Trader Joe's Organic Chicken Thighs" won't match cleanly no matter how it's normalized. The .toLowerCase().replace(/\s+/g, "_") normalization above handles simple cases; it does not solve the deeper problem of a free-text product name not lining up with a curated recipe database's own fixed vocabulary. This app's own honest scope stops at "best-effort matching," not guaranteed matches for every real product name.
The React Results Component
ingredient rather than by the whole combination of expiring items means a cached "chicken" lookup gets reused the next time chicken appears in the alerts list, regardless of what else happened to be expiring alongside it that day — the same granular-caching principle as Chapter 3's own barcode_cache.
Where This Course Is Headed
State management across the whole app next — where component-local state ends and a shared approach begins, now that scanning, alerts, history, and recipes all need to talk to each other.
Hands-On Exercises
Explain the concrete difference in behavior between Promise.all(expiring.map(...)) and a for loop that awaits each lookupIngredient call one at a time, for five expiring ingredients.
📄 View solutionExplain why a generic pantry item name like "Trader Joe's Organic Chicken Thighs" might fail to match anything in TheMealDB even after normalization, and why this is described as an honest scope limit rather than a bug to fix.
📄 View solutionExplain why recipe_cache is keyed by ingredient rather than by the full combination of expiring items on any given day, and what benefit that specific choice provides.
📄 View solutionChapter 9 Quick Reference
- Route: GET /api/recipes/suggest — fans out to TheMealDB per expiring ingredient, merges and sorts by match count
- New table: recipe_cache, keyed by ingredient, storing the JSON meal list as text
- Real advantage: Promise.all fires every ingredient lookup concurrently — a genuine payoff of Node's async model, vs. Food Tracker (Django)'s own honestly-named sequential fan-out
- Real gotcha: TheMealDB expects its own exact ingredient vocabulary — generic product names often won't match cleanly even after normalization
- Next chapter: State Management Across the App