Recipe Lookup with TheMealDB

Food Tracker (Django)

Chapter 10 · Recipe Lookup with TheMealDB

This chapter delivers the last named feature from Chapter 1's original spec, reusing the same integration pattern Chapter 4 already established — and running into a real, honest limitation specific to how this course's views are built.

The Same Limitation, Regardless of Framework

TheMealDB's filter-by-ingredient endpoint searches exactly one ingredient per request — this isn't a Django, FastAPI, or Firebase-specific constraint at all, it's simply a property of the external API itself, identical across all four Food Tracker courses. Every one of them has to fan out one request per expiring ingredient and merge the results afterward.

A View That Fans Out

def suggest_recipes(request): expiring = Item.objects.filter(status="active", expiry_date__lte=threshold) ingredients = [item.name for item in expiring] merged = {} for ingredient in ingredients: response = requests.get( f"https://www.themealdb.com/api/json/v1/1/filter.php?i={ingredient}" ) data = response.json() for meal in (data.get("meals") or []): meal_id = meal["idMeal"] if meal_id not in merged: merged[meal_id] = {**meal, "matched_ingredients": []} merged[meal_id]["matched_ingredients"].append(ingredient) results = sorted( merged.values(), key=lambda m: len(m["matched_ingredients"]), reverse=True ) return JsonResponse(results, safe=False)

A Real Cost of This Sequential Loop

Each requests.get() call here runs one after another, and this ordinary Django view blocks entirely until every single one finishes — five expiring ingredients means roughly five times the wait of firing them all at once. This is a genuine, honest cost specific to how this view is written: Food Tracker (FastAPI)'s own async patterns and Food Tracker (React + Firebase)'s Promise.all() both fire every ingredient lookup concurrently instead. Django does support async views (async def, since Django 3.1), but doing this properly would also require swapping the synchronous requests library for an async-compatible HTTP client — real additional complexity genuinely beyond this chapter's own scope, named honestly rather than glossed over.

Relevance Sorting — The One Place All Four Courses Converge

Sorting by len(matched_ingredients) descending surfaces recipes using the most expiring items first — directly serving Chapter 1's original point: using up as much soon-to-expire food as possible in one meal. This particular piece of logic is functionally identical across all four Food Tracker courses, each expressed in its own language's idioms — a genuine convergence point after several chapters of real, honest divergence.

Caching, Same Reasoning, a New Kind of Column

class RecipeCache(models.Model): ingredient = models.CharField(max_length=200, primary_key=True) meals_json = models.JSONField()

JSONField (native since Django 3.1, backed by real JSON column support in modern PostgreSQL, MySQL, and SQLite) is a genuinely interesting nuance worth naming directly: even a relational, schema-on-write framework has its own escape hatch for storing semi-structured, document-like data in a single column, when that's honestly the better fit — a list of matched-meal dictionaries here doesn't cleanly decompose into further normalized tables for this app's own modest needs. Relational and document approaches aren't a strict either/or; a real relational database can hold document-shaped data exactly where that's the more sensible choice.

This time, the other courses have the advantage
Chapters 7 and 8 credited this course's own SQL-based approach with real, fair advantages over the Firebase sibling. This chapter is the honest opposite case: a naive sequential loop here is genuinely slower than the concurrent fan-out both the FastAPI and Firebase courses use for the identical task. Keeping the comparison honest means naming a real cost when there is one, not just the wins.
JSONField is worth knowing about generally
Beyond this one cache, it's the right tool anytime a piece of data is genuinely variable in shape or doesn't need its own normalized table — a small, real escape hatch inside an otherwise strict, columnar model.
This view is genuinely slower than it needs to be
The sequential loop above blocks on every single TheMealDB request in turn. This is a known, real limitation of the simple version built in this chapter — not a hidden defect, but a cost worth being honest about rather than assuming a synchronous view is automatically "fine" regardless of how many external calls it makes.

Where This Course Is Headed

Django REST Framework, deployment, and a capstone.

Hands-On Exercises

Exercise 1

Explain why this view takes roughly N times as long as necessary for N expiring ingredients, and what the FastAPI and Firebase sibling courses do differently to avoid this specific cost.

📄 View solution
Exercise 2

Explain how the matched_ingredients sort serves Chapter 1's original recipe-lookup intent, and why this chapter calls this one piece of logic essentially identical across all four Food Tracker courses.

📄 View solution
Exercise 3

Explain what JSONField is, and why using it for RecipeCache doesn't actually contradict Django's own relational, schema-on-write identity.

📄 View solution

Chapter 10 Quick Reference

  • One ingredient per request — TheMealDB's own limitation, identical across all four courses
  • Sequential, blocking loop — a real, honest cost; FastAPI's async and Firebase's Promise.all() both avoid it by running concurrently
  • matched_ingredients sort — the one piece of logic functionally identical across the whole quartet
  • JSONField — Django's own escape hatch for document-shaped data inside a relational schema
  • Next chapter: Django REST Framework: Exposing an API Layer