Recipe Lookup with TheMealDB
Food Tracker (React + Express + Prisma)
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 — reusing Chapter 3's own caching pattern, and Chapter 1's own concurrency story, largely unchanged.
Extending the Schema: A Recipe Cache
The same shape as Chapter 3's own BarcodeCache, keyed by ingredient this time:
Json field type — but checked directly against real, current Prisma issue history, its support for the SQLite provider specifically has been genuinely inconsistent across versions, with open bug reports even where partial support exists. Rather than depend on a still-uneven feature, this model stores the meal list the same way the sibling course does — as a plain string, serialized and parsed by hand — for portability and to avoid a dependency this course can't fully vouch for.
The Suggestion Route
upsert() for the cache write is the exact same choice Chapter 3 made for BarcodeCache, and for the same reason — it safely handles two concurrent lookups of the same never-before-cached ingredient, where a plain create could collide on the ingredient primary key. select: { name: true } narrows the alerts-style query to just the one field this route actually needs, matching the sibling's own SELECT name FROM items — both courses already avoid pulling the whole row here.
Promise.all fires every ingredient's lookup concurrently, because Node's own async model makes that the natural way to write it. This win comes from Node and JavaScript, not from Prisma — it's identical in this course and its raw-SQL sibling, since both are Express apps running on the same runtime.
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
Unchanged from the sibling course — nothing here touches a date field or a naming convention that differs between the two courses:
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 BarcodeCache.
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 RecipeCache stores meals as a plain String rather than Prisma's own Json field type, given what this chapter found checking Prisma's real issue history.
📄 View solutionExplain why the cache write here uses upsert(), tracing the reasoning back to the specific chapter that first introduced this pattern and the specific problem it solves.
📄 View solutionChapter 9 Quick Reference
- Route: GET /api/recipes/suggest — fans out to TheMealDB per expiring ingredient, merges and sorts by match count
- New model: RecipeCache, keyed by ingredient, meals stored as a JSON-encoded String (not Prisma's Json type — inconsistent SQLite support, checked directly)
- Reused pattern: upsert() for the cache write, same as Chapter 3's BarcodeCache, same reason
- Real advantage, unrelated to Prisma: Promise.all fires every ingredient lookup concurrently — a Node/JavaScript fact, identical in both this course and its raw-SQL sibling
- Real gotcha, unchanged: 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