Recipe Lookup with TheMealDB
Food Tracker (FastAPI)
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 every lesson Chapter 3 already taught about proxying an external API, plus one genuinely new gotcha this specific combination introduces.
A Recipe Cache, Same Pattern as Chapter 3
Real Concurrency via asyncio.gather()
Multiple ingredients need looking up at once — the same fan-out shape every sibling course faces. FastAPI's own async model makes genuine concurrency the natural way to write this, not a special optimization bolted on afterward:
Depends(get_db) — perfectly fine when only one query runs at a time. asyncio.gather() runs several lookup_ingredient calls concurrently, and SQLAlchemy's classic Session is explicitly not designed to be shared across concurrently-running tasks — interleaved queries and commits against one shared session can leave its internal state genuinely inconsistent. The fix above is deliberate: lookup_ingredient opens and closes its own dedicated SessionLocal() rather than reusing the route's own db parameter, giving each concurrent task a session entirely its own. This is a real, specific consequence of combining FastAPI's async concurrency with SQLAlchemy's synchronous session model — not a gotcha any of this quartet's other three courses have to think about in quite the same way.
asyncio.gather() here fires every ingredient's lookup concurrently instead, the exact same category of advantage Food Tracker (React + Express)'s own Promise.all already claimed for itself in that course's own Chapter 9 — two genuinely different languages, Python and JavaScript, each with a real async model, both reaching the same concurrent result for the same underlying reason: an async-first runtime turns out to matter for more than just how a single request feels to write.
ingredient 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 every sibling course's own recipe cache.
Where This Course Is Headed
Async patterns and background tasks next — a deeper look at async def, BackgroundTasks, and connection-pooling considerations, building directly on this chapter's own concurrency work.
Hands-On Exercises
Explain why sharing the route's own Depends(get_db) session across every concurrent lookup_ingredient call would be unsafe, and what lookup_ingredient does instead to avoid the problem.
📄 View solutionExplain why this course and Food Tracker (React + Express) both achieve real concurrent fan-out for the same underlying reason, despite being written in two completely different languages.
📄 View solutionExplain why recipe_cache is keyed by ingredient rather than by the full combination of expiring items on any given day.
📄 View solutionChapter 9 Quick Reference
- Route: GET /suggest — asyncio.gather() 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 gotcha: a shared synchronous SQLAlchemy Session is unsafe across concurrent asyncio tasks — each lookup_ingredient call opens and closes its own dedicated session
- Real advantage: asyncio.gather() matches Food Tracker (React + Express)'s own Promise.all — two of four courses achieve genuine concurrency here, vs. Food Tracker (Django)'s own honestly-named sequential cost
- Next chapter: Async Patterns & Background Tasks