Introduction: Enhancing User Experience with Dynamic Recipes

Welcome back! So far, you have learned how to build API endpoints that allow users to retrieve recipes, search by ingredients, and view recipe steps. These features are essential for any recipe app; however, users sometimes desire more variety or guidance. For example, what if a user cannot decide what to cook and wants a random suggestion? Or perhaps they want to see the most popular recipes based on ratings from other users.

In this lesson, you will learn how to add two new features to your cooking helper API:

  • An endpoint that returns a random recipe
  • An endpoint that returns the most popular recipes based on user ratings

These features make your app more interactive and helpful, providing users with new ways to discover recipes.

Quick Recall: Retrieving and Filtering Recipes

Before we proceed, let us briefly review how we have been working with recipes so far.

Previously, you built endpoints to perform the following tasks:

  • List all recipes with pagination.
  • Retrieve a single recipe by its ID.
  • Search for recipes by ingredients.

All these endpoints follow a standard pattern: we map a URL path to a view function in urls.py and, within that function, we use QuerySets to interact with the database. You have also used helper functions like serialize_recipe to convert database objects into dictionaries, which are then returned as a JsonResponse.

Now, let us build upon these skills to add dynamic selection features.

Building the Random Recipe Endpoint

Let us start by creating an endpoint that returns a random recipe. This feature is useful for users who want to try something new or cannot decide what to cook.

Step 1: Setting Up the Route

First, we must define a new route. Unlike previous lessons where we used parameters like <int:recipe_id>, this path is static. In cooking_helper/recipes/urls.py, we add:

path("recipes/random", views.get_random_recipe),

Then, in views.py, we define the function. Every view function must accept a request object as its first argument.

def get_random_recipe(request):
    # Implementation will go here
Step 2: Selecting a Random Recipe

To select a random recipe from the database, we use a special ordering character: the question mark "?". When we apply .order_by("?") to a QuerySet, the database shuffles the records.

recipe = Recipe.objects.order_by("?").prefetch_related("ingredients").first()
  • Recipe.objects.order_by("?") shuffles the order randomly.
  • .prefetch_related("ingredients") ensures we efficiently load the ingredient data.
  • .first() picks the first recipe from the shuffled list.

If there are no recipes in the database, the recipe variable will be None.

Step 3: Returning the Recipe as a Response

Now, let us check if we found a recipe. If the database is empty, we return a 404 error using a helper function like _json_error. If a recipe is found, we serialize it and return it using JsonResponse.

if not recipe:
    return _json_error("No recipes found", 404)

return JsonResponse(serialize_recipe(recipe))
Full Example

Here is how the route and the view function look together:

cooking_helper/recipes/urls.py

from django.urls import path
from . import views

urlpatterns = [
    # ... other paths ...
    path("recipes/random", views.get_random_recipe),
]

cooking_helper/recipes/views.py

@require_http_methods(["GET"])
def get_random_recipe(request):
    recipe = Recipe.objects.order_by("?").prefetch_related("ingredients").first()
    if not recipe:
        return _json_error("No recipes found", 404)
    return JsonResponse(serialize_recipe(recipe))

Example Output:

{
  "id": 3,
  "name": "Spaghetti Carbonara",
  "ingredients": ["spaghetti", "eggs", "bacon", "parmesan"],
  "steps": "Boil pasta\nCook bacon\nMix eggs and cheese\nCombine all"
}
Building the Popular Recipes Endpoint

Next, let us create an endpoint that returns the most popular recipes. We will define "popular" as recipes with the highest average user ratings.

Step 1: Setting Up the Route

We will add a GET endpoint at /api/recipes/popular in our urls.py configuration.

path("recipes/popular", views.get_popular_recipes),
Step 2: Calculating Average Ratings

To find popular recipes, we must calculate the average rating for each recipe. We use the annotate() method combined with the Avg aggregation function to add a virtual field called avg_rating to our QuerySet.

from django.db.models import Avg

queryset = Recipe.objects.annotate(avg_rating=Avg("reviews__rating"))

This tells the database to look at the related reviews, calculate the average of their rating fields, and attach that value to each recipe.

Step 3: Filtering and Sorting

We only want recipes that actually have ratings, and we want the top 10 highest-rated ones.

queryset = (
    Recipe.objects.annotate(avg_rating=Avg("reviews__rating"))
    .filter(avg_rating__isnull=False)
    .order_by("-avg_rating")
    .prefetch_related("ingredients")
)
recipes = list(queryset[:10])
  • .filter(avg_rating__isnull=False) excludes recipes with no reviews.
  • .order_by("-avg_rating") sorts the results in descending order (highest rating first).
  • [:10] limits the results to the top 10 items.
Step 4: Formatting the Output

Since we are returning a list of objects, we must iterate through the recipes results, prepare the data, and use JsonResponse with safe=False.

response = []
for recipe in recipes:
    data = serialize_recipe(recipe)
    avg_rating = recipe.avg_rating
    data["average_rating"] = round(float(avg_rating), 2) if avg_rating is not None else None
    response.append(data)

return JsonResponse(response, safe=False)
Full Example

Here is the complete implementation for the popular recipes feature:

cooking_helper/recipes/views.py

@require_http_methods(["GET"])
def get_popular_recipes(request):
    queryset = (
        Recipe.objects.annotate(avg_rating=Avg("reviews__rating"))
        .filter(avg_rating__isnull=False)
        .order_by("-avg_rating")
        .prefetch_related("ingredients")
    )
    recipes = list(queryset[:10])

    response = []
    for recipe in recipes:
        data = serialize_recipe(recipe)
        avg_rating = recipe.avg_rating
        data["average_rating"] = round(float(avg_rating), 2) if avg_rating is not None else None
        response.append(data)

    return JsonResponse(response, safe=False)

Example Output:

[
  {
    "id": 2,
    "name": "Chicken Alfredo",
    "ingredients": ["chicken", "pasta", "cream", "parmesan"],
    "steps": "...",
    "average_rating": 4.75
  },
  {
    "id": 5,
    "name": "Vegetable Stir Fry",
    "ingredients": ["broccoli", "carrots", "soy sauce", "rice"],
    "steps": "...",
    "average_rating": 4.5
  }
]
Summary and What’s Next

In this lesson, you learned how to add two new endpoints to your recipe API:

  • One for serving a random recipe using .order_by("?") to help users discover new dishes.
  • One for listing the most popular recipes using .annotate() and Avg to rank dishes by user feedback.

These features make your app more dynamic and user-friendly. You are now ready to practice building these endpoints in the exercises that follow!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal