Introduction: Enhancing Our Cooking API

Welcome back! In the last lesson, you learned how to make your cooking assistant more accessible by generating audio instructions. Now, we are going to make your cooking API even more useful and interactive by adding two new features: the ability to list all available ingredients and to let users submit reviews for recipes.

These features are important for any smart cooking app. Listing ingredients helps users see what’s available or filter recipes, while reviews allow users to share feedback and help others find the best recipes. By the end of this lesson, you will know how to build and test these endpoints using a modern API framework.

Quick Recall: API Endpoints Refresher

Before we dive in, let’s quickly remind ourselves how endpoints work in this framework. Instead of defining the URL and the logic in the same place, we split them into two files:

  1. urls.py: This file handles routing. It maps a URL path (like /hello) to a specific function.
  2. views.py: This file contains the logic. It defines functions (called "views") that take a request and return a response.

To restrict which HTTP methods (like GET or POST) an endpoint accepts, we use decorators like @require_http_methods.

For example, here’s a simple GET endpoint:

urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("hello", views.say_hello),
]

views.py

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods

@require_http_methods(["GET"])
def say_hello(request):
    return JsonResponse({"message": "Hello, world!"})

In this lesson, you’ll use this pattern to build your new endpoints.

Building the Ingredients Endpoint

Let’s start by creating an endpoint that lists all unique ingredients in your database. This will be a GET endpoint, which means users will request data from it.

Step 1: Define the Route and View

First, you need to define the function in views.py and map it to a path in urls.py. The view function always takes a request object as its first argument.

views.py

@require_http_methods(["GET"])
def get_all_ingredients(request):
    # Implementation will go here

urls.py

path("ingredients", views.get_all_ingredients),
Step 2: Query the Database

Next, you want to get all unique ingredient names from the database. You can use the ORM (Object-Relational Mapper) to do this efficiently:

names = Ingredient.objects.values_list("name", flat=True).distinct()
  • Ingredient.objects.values_list("name", flat=True) asks the database for just the values of the name column.
  • .distinct() ensures you only get each name once.
Step 3: Format the Results

The ORM query returns a special QuerySet object. To return this as a JSON response, we convert it into a standard list.

names_list = list(names)
return JsonResponse(names_list, safe=False)
  • list(names) converts the database result into a simple list of strings.
  • safe=False is required in JsonResponse whenever you are returning a list (an array) instead of a dictionary (an object) at the top level.
Full Example

Here is how the implementation looks across your files:

recipes/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("ingredients", views.get_all_ingredients),
]

recipes/views.py

from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from .models import Ingredient

@require_http_methods(["GET"])
def get_all_ingredients(request):
    names = list(Ingredient.objects.values_list("name", flat=True).distinct())
    return JsonResponse(names, safe=False)

Example Output:

If your database has eggs, flour, and milk, a GET request to /ingredients will return:

["egg", "flour", "milk"]
Building the Reviews Endpoint

Now, let’s add a way for users to submit reviews for recipes. This will be a POST endpoint. Because we are receiving data, we need to handle manual JSON parsing and security.

Step 1: Define the Route and Decorators

To handle a POST request for data submission, we use two important decorators:

  1. @csrf_exempt: This allows external tools or frontends to send POST requests to our API without needing a security token (common for purely programmatic APIs).
  2. @require_http_methods(["POST"]): This ensures the endpoint only reacts to POST requests.
@csrf_exempt
@require_http_methods(["POST"])
def submit_review(request):
    # Logic goes here
Step 2: Get and Validate the Data

Since the data is sent in the request body as JSON, we must parse it manually. We also need to check if the user provided the required fields and if the rating is within the valid range (1 to 5).

import json

# Parse the body
payload = json.loads(request.body.decode("utf-8"))
recipe_id = payload.get("recipe_id")
rating = payload.get("rating")

# Basic validation
if rating < 1 or rating > 5:
    return JsonResponse({"detail": "rating must be between 1 and 5"}, status=422)

We also check whether the recipe being reviewed actually exists in our database:

recipe = Recipe.objects.filter(pk=recipe_id).first()
if not recipe:
    return JsonResponse({"detail": "Invalid recipe_id"}, status=400)
  • .filter(pk=recipe_id).first() looks for a recipe where the primary key matches the ID. If none is found, it returns None.
Step 3: Save the Review and Return Confirmation

If the data is valid, we create a new record in the database and return a 201 Created status code.

Review.objects.create(recipe=recipe, rating=rating)
return JsonResponse({"message": "Review submitted successfully"}, status=201)
  • Review.objects.create() builds and saves the object in one step.
  • status=201 explicitly tells the user that a new resource was created.
Full Example

Here is the complete implementation for the reviews endpoint, including a helper for parsing:

recipes/views.py

import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from .models import Recipe, Review

@csrf_exempt
@require_http_methods(["POST"])
def submit_review(request):
    # Parse JSON body
    try:
        payload = json.loads(request.body.decode("utf-8"))
    except json.JSONDecodeError:
        return JsonResponse({"detail": "Invalid JSON body"}, status=400)

    recipe_id = payload.get("recipe_id")
    rating = payload.get("rating")

    if recipe_id is None or rating is None:
        return JsonResponse({"detail": "recipe_id and rating are required"}, status=400)

    # Validate rating range
    try:
        rating = int(rating)
        if rating < 1 or rating > 5:
            return JsonResponse({"detail": "rating must be between 1 and 5"}, status=422)
    except (TypeError, ValueError):
        return JsonResponse({"detail": "rating must be an integer"}, status=400)

    # Check if recipe exists
    recipe = Recipe.objects.filter(pk=recipe_id).first()
    if not recipe:
        return JsonResponse({"detail": "Invalid recipe_id"}, status=400)

    # Save to database
    Review.objects.create(recipe=recipe, rating=rating)
    
    return JsonResponse({"message": "Review submitted successfully"}, status=201)

Example Input:

A user sends this JSON to /reviews:

{
  "recipe_id": 1,
  "rating": 5
}

Example Output:

{
  "message": "Review submitted successfully"
}
Summary and Practice Preview

In this lesson, you learned how to add two important features to your smart cooking API: listing all unique ingredients and allowing users to submit reviews. You saw how to:

  • Map URLs in urls.py to logic in views.py.
  • Use ORM methods like values_list and distinct.
  • Manually parse JSON from a request body and perform validation.
  • Return appropriate status codes like 201 for created resources and 422 or 400 for errors.

Next, you’ll get a chance to practice these skills by building and testing similar endpoints on your own!

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