Introduction: The Role of Recipe Retrieval in Your App

Welcome to the first lesson of the Building the Recipe API course! In this lesson, you will learn how to build the core endpoints that allow your app to fetch recipes and their details from a database.

Recipe retrieval is a fundamental feature of any cooking helper app. Users need to be able to look up recipes, see what ingredients are required, and follow the steps to cook a dish. By the end of this lesson, you will know how to create endpoints that return a single recipe by its ID and a list of recipes with pagination. These skills are the foundation for more advanced features you’ll add later, such as searching, filtering, and user reviews.

Recall: API Routes and Database Models

Before we dive in, let’s briefly recall two key concepts you’ll use in this lesson: API routes and database models.

  • API routes define the URLs of your web application and map them to specific logic. In this framework, you define the URL patterns in a urls.py file and the logic inside functions called views in a views.py file. For example, when a user visits /api/recipes/1, a view function will handle that request and return the recipe with ID 1.
  • Database models are classes that represent tables in your database. For example, you might have a Recipe model for recipes and a Review model for user reviews. These models allow you to interact with your data using objects and methods instead of writing raw database queries.

You’ll see both of these concepts in action as we build the recipe retrieval endpoints.

Building the Single Recipe Endpoint

Let’s start by building an endpoint that returns a single recipe by its ID. This is useful when a user wants to view the details of a specific recipe.

Step 1: Defining the Route

You define a route by adding a path to the urlpatterns list in your urls.py file. Here’s how you set up a route to handle requests like /api/recipes/1:

# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("recipes/<int:recipe_id>", views.get_recipe),
]
  • The <int:recipe_id> part is a path converter. It matches an integer value in the URL and passes it to the get_recipe view function as an argument named recipe_id.
Step 2: Querying the Recipe

Inside the view function in views.py, you need to fetch the recipe from the database. You can use the model's manager to filter for the specific ID:

def get_recipe(request, recipe_id: int):
    recipe = Recipe.objects.filter(pk=recipe_id).prefetch_related("ingredients").first()
    if not recipe:
        return JsonResponse({"detail": "Recipe not found"}, status=404)
    # logic continues...
  • Recipe.objects.filter(pk=recipe_id) searches for the recipe.
  • The pk stands for primary key, which is often the ID.
  • .prefetch_related("ingredients") is used to efficiently load the related ingredients in a single step.
  • .first() returns the object if found, or None if it doesn't exist. If it is missing, we return a JsonResponse with a 404 status.
Step 3: Calculating the Average Rating

Suppose your app allows users to rate recipes. You can calculate the average rating by using aggregation:

from django.db.models import Avg

avg_rating = Review.objects.filter(recipe_id=recipe.id).aggregate(avg=Avg("rating"))["avg"]
average = round(float(avg_rating), 2) if avg_rating is not None else None
  • Review.objects.filter(...) gets all reviews for this specific recipe.
  • .aggregate(avg=Avg("rating")) calculates the average of the rating column.
  • If there are no ratings, the result is None; otherwise, we round it to two decimal places.
Step 4: Building the JSON Response

To send data back to the user, you must convert the database object into a format that can be turned into JSON (usually a dictionary). A utility function is a great way to handle this serialization:

# utils.py
def serialize_recipe(recipe):
    return {
        "id": recipe.id,
        "name": recipe.name,
        "ingredients": [ingredient.name for ingredient in recipe.ingredients.all()],
        "steps": recipe.steps,
    }

# views.py
data = serialize_recipe(recipe)
data["average_rating"] = average
return JsonResponse(data)
  • The serialize_recipe function extracts the necessary fields into a dictionary.
  • JsonResponse then takes this dictionary and sends it to the user as a JSON object.

Example Output:

{
  "id": 1,
  "name": "Spaghetti Carbonara",
  "ingredients": ["spaghetti", "eggs", "bacon", "parmesan"],
  "steps": "1. Boil pasta. 2. Cook bacon. 3. Mix eggs and cheese. 4. Combine all."
}
Building the Paginated Recipes List Endpoint

Next, let’s build an endpoint that returns a list of recipes, but only a few at a time. This is called pagination, and it helps your app handle large numbers of recipes efficiently.

Step 1: Defining the Paginated Route

First, add the route to urls.py:

path("recipes", views.get_all_recipes_paginated),

In the view function, you can extract pagination parameters from the URL's query string (e.g., /api/recipes?page=2&per_page=5) using request.GET:

def get_all_recipes_paginated(request):
    page = int(request.GET.get("page", 1))
    per_page = int(request.GET.get("per_page", 10))
    # logic continues...
  • request.GET.get("page", 1) attempts to get the page parameter, defaulting to 1 if it is missing.
Step 2: Querying and Paginating Recipes

To get the correct slice of data, you calculate an offset and use slicing on the database QuerySet:

total = Recipe.objects.count()
offset = (page - 1) * per_page

items = (
    Recipe.objects.all()
    .prefetch_related("ingredients")
    .order_by("id")[offset : offset + per_page]
)
  • total is the total number of recipes in the database.
  • [offset : offset + per_page] tells the database to skip the recipes from previous pages and only return the number of items specified by per_page.
Step 3: Building the Paginated Response

Finally, construct a dictionary containing the list of recipes and metadata about the pagination:

import math

pages = math.ceil(total / per_page) if per_page else 0

return JsonResponse(
    {
        "recipes": [serialize_recipe(r) for r in items],
        "total": total,
        "page": page,
        "per_page": per_page,
        "pages": pages,
        "has_next": page < pages,
        "has_prev": page > 1,
    }
)
  • We use a list comprehension to call serialize_recipe on every item in the current page.
  • The response includes helper data like has_next and has_prev so the frontend knows if it should display "Next" or "Previous" buttons.

Example Output:

{
  "recipes": [
    {
      "id": 1,
      "name": "Spaghetti Carbonara",
      "ingredients": ["spaghetti", "eggs", "bacon", "parmesan"],
      "steps": "..."
    },
    {
      "id": 2,
      "name": "Tomato Soup",
      "ingredients": ["tomatoes", "onion", "garlic", "cream"],
      "steps": "..."
    }
  ],
  "total": 25,
  "page": 1,
  "per_page": 2,
  "pages": 13,
  "has_next": true,
  "has_prev": false
}
Summary and What’s Next

In this lesson, you learned how to build two essential endpoints for your cooking helper app:

  • One to fetch a single recipe by its ID, including its ingredients and average rating, by mapping a URL path to a view function.
  • Another to list all recipes with manual pagination using QuerySet slicing and query parameters.

These endpoints are the backbone of your app’s recipe retrieval features. In the practice exercises that follow, you’ll get hands-on experience implementing these endpoints, defining routes in urls.py, and returning data using JsonResponse. Good luck, and have fun building!

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