Welcome back! In the last lesson, you learned how to build basic endpoints to retrieve recipes from your database, including fetching a single recipe by its ID and listing recipes with pagination. These are the building blocks of any recipe API.
Now, let’s take things a step further. Imagine you open a cooking app and want to search for recipes by a list of ingredients that you have in your kitchen. Or maybe you want to return recipe steps as a list so you can follow them one by one. These features make your app much more helpful and user-friendly.
In this lesson, you’ll learn how to:
- Search for recipes by a list of ingredients.
- Return recipe steps as a list, not just a long string.
These skills will help you build a smarter, more flexible recipe API.
Before we dive in, let’s quickly remind ourselves how our data is organized. This will help you understand how searching and filtering work.
Recipe: Each recipe has aname, astepsfield (a long text field), and a list ofingredients.Ingredient: Each ingredient has aname.- Relationship:
IngredientsandRecipesare linked through a Many-to-Many relationship. This means one recipe can have many ingredients, and one ingredient can belong to many recipes.
Here is how the models are defined:
This structure is important because it allows us to filter recipes based on the ingredients they are connected to.
Many apps and websites want to show recipe steps one at a time, not as a single long string. To help with this, you can create an endpoint that returns the steps as a JSON list.
First, we create a view function that takes a recipe_id from the URL. We use the .first() method to find the specific recipe:
- If the recipe does not exist, we return a
404 Not Foundresponse.
In our database, steps is stored as a single string where each step is on a new line. We can split this string into a list using the newline character \n.
split("\n")breaks the text into individual lines.strip()ensures we don't include extra spaces at the beginning or end of a step.- The
if step.strip()part ensures we don't include empty lines in our final list.
Finally, we return a JsonResponse containing the id, the name, and the newly created steps_list.
Example output:
Let’s say you want to find all recipes that use a specific set of ingredients, like tomato and basil. You’ll need an endpoint that takes a list of ingredient names and returns only the recipes that use all of them.
Since we are sending a list of ingredients, we use a POST request. We need to manually parse the JSON body from the request.
To find recipes containing all requested ingredients, we use annotate and Count. This allows us to count how many of our "target" ingredients each recipe has.
filter(ingredients__name__in=...): Initially narrows the list to recipes that have at least one of the ingredients.annotate(...): Adds a temporary field calledmatch_countto each recipe. It counts how many ingredients in that recipe match our provided list.filter(match_count=len(ingredient_names)): This is the crucial part. If we send 2 ingredients, we only want recipes where thematch_countis exactly2.prefetch_related("ingredients"): Optimizes the query so we can fetch ingredientnameslater without hitting the database repeatedly.
To return the recipes, we convert the database objects into dictionaries.
Example input:
Example output:
In this lesson, you learned how to:
- Use string manipulation to return recipe
stepsas a clean list. - Handle
POSTrequests and parseJSONbodies manually. - Perform complex filtering using
annotate,Count, andQobjects to find recipes matching a specific list of ingredients.
These features make your recipe API much more useful and user-friendly. Up next, you’ll get to practice building and testing these endpoints yourself. Good luck!
