Skip to content

Exercise: The Animals API

You'll:

  • paginate the API's list
  • filter the list by species
  • link each animal to its own endpoint
  • run the tests, and look at the API in the browser and with curl

What's provided in your branch:

  • Django REST Framework, installed and configured, with the api/v1/ include
  • AnimalSerializer, AnimalViewSet, and the router
  • the REST_FRAMEWORK setting and get_queryset(), with TODOs to fill in
  • tests for the API

Before You Start

Stop the development server with Ctrl+C if it's running. Then set up the starting state for this exercise:

shell
git stash -u
git fetch --all
git checkout -b my-ch07 --no-track origin/ch07
uv sync

If Git prints No local changes to save, there was nothing to set aside.

Files that changed in this chapter:
  • pyproject.toml (modified, adds Django REST Framework)
  • config/settings.py (modified, adds rest_framework to INSTALLED_APPS, and the REST_FRAMEWORK setting; you'll paginate the API's list)
  • config/urls.py (modified, includes the API under api/v1/)
  • animals/serializers.py (new, AnimalSerializer; you'll add a link to its fields)
  • animals/api.py (new, AnimalViewSet; you'll filter it by species)
  • animals/api_urls.py (new, the router)
  • tests/test_api.py (new)
  • tests/conftest.py (modified, keeps uploaded photos in a temporary folder)
  • tests/test_forms.py (removed)
  • AGENTS.md (modified, notes that the API is Django REST Framework and open on purpose)
  • README.md (modified, describes the API)

Step 1: Paginate the List

Open config/settings.py. Below STATICFILES_DIRS, the REST_FRAMEWORK setting is an empty dictionary with a TODO:

config/settings.py
# --- Django REST Framework ---

REST_FRAMEWORK = {
    # TODO: Set DEFAULT_PAGINATION_CLASS to
    # "rest_framework.pagination.PageNumberPagination" and PAGE_SIZE to 12.
}

Step 2: Filter by Species

Open animals/api.py. The browsable API shows AnimalViewSet's docstring at the top of the list's page, and get_queryset() has a TODO for the filter:

animals/api.py
class AnimalViewSet(viewsets.ReadOnlyModelViewSet):
    """Every animal, newest arrivals first.

    Anyone can read this API: DRF's default permission, AllowAny, lets every
    request through. A production API would usually require a login or rate
    limiting.

    Add an animal's id to the address for one animal, or ?species=rabbit
    for one species.
    """

    queryset = queries.all_animals()
    serializer_class = AnimalSerializer

    def get_queryset(self):
        animals = super().get_queryset()
        species = self.request.query_params.get("species")
        # TODO: When there's a species, narrow animals with queries.of_species().
        return animals

queries.of_species(animals, species), in animals/queries.py, is the query the list page's search uses, so the page and the API narrow the animals the same way.

Open animals/serializers.py. AnimalSerializer's fields are the model's, with age_label, and a TODO at the top of the list:

animals/serializers.py
class AnimalSerializer(serializers.ModelSerializer):
    class Meta:
        model = Animal
        fields = [
            # TODO: Add "url", which links each animal to its own endpoint.
            "id",
            "name",
            "species",
            "breed",
            "birth_date",
            "age_label",
            "description",
            "photo",
            "status",
            "intake_date",
        ]

Step 4: Run the Tests

shell
uv run pytest

All tests should pass.

Step 5: Look at the API

Start the server with uv run manage.py runserver, and try:

  • http://127.0.0.1:8000/api/v1/: the API root, with a link to the animals endpoint.
  • The animals link: the first page of the list, with:
    • the total count
    • the next page's link
    • previous, null on the first page
    • the page's results, each animal with its url and age_label
  • An animal's url: that one animal.
  • http://127.0.0.1:8000/api/v1/animals/?species=rabbit: only rabbits, with a count to match.
  • The same list from the command line, as one line of JSON:

    shell
    curl "http://127.0.0.1:8000/api/v1/animals/?species=rabbit"
    

Success

Your API is working if the tests pass and the browser shows the root, the list's pages, an animal, and only rabbits for ?species=rabbit.

Troubleshooting Tips

TypeError: list indices must be integers or slices, not str

The list isn't paginated, so its JSON is a list instead of an object with results. DRF ignores a key in REST_FRAMEWORK it doesn't know: check that both DEFAULT_PAGINATION_CLASS and PAGE_SIZE are set and spelled as in Step 1's TODO.

ImportError: Could not import '...' for API setting 'DEFAULT_PAGINATION_CLASS'

The class's path is misspelled: it's "rest_framework.pagination.PageNumberPagination". The error stops pytest while it collects the tests, before any test runs, and stops the development server too.

?species=rabbit returns every animal

get_queryset() doesn't narrow animals. Assign the result: animals = queries.of_species(animals, species).

The list is empty without ?species=

The filter runs even when species is None, and of_species() returns no animals for that value. Narrow animals only when species has a value, with if species:. Each animal's own endpoint returns 404 for the same reason: retrieve looks the animal up in get_queryset().

ImproperlyConfigured: Field name ... is not valid for model Animal

A name in fields isn't a field or a method on Animal. Check the spelling of "url"; a missing comma between two names joins them into one string.

test_animal_detail_fields fails with Extra items in the right set

A name is missing from fields. The test compares the keys of an animal's JSON with the full list, and pytest prints the missing name under that line.

Compare with the Solution

shell
git diff solutions/ch07 -- config/settings.py animals/api.py animals/serializers.py

See the solution on GitHub: config/settings.py, animals/api.py, and animals/serializers.py.