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_FRAMEWORKsetting andget_queryset(), withTODOs 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:
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, addsrest_frameworktoINSTALLED_APPS, and theREST_FRAMEWORKsetting; you'll paginate the API's list)config/urls.py(modified, includes the API underapi/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:
# --- 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:
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.
Step 3: Link Each Animal
Open animals/serializers.py. AnimalSerializer's fields are the model's, with age_label, and a TODO at the top of the list:
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
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
animalsendpoint. - The
animalslink: the first page of the list, with:- the total
count - the
nextpage's link previous,nullon the first page- the page's
results, each animal with itsurlandage_label
- the total
- An animal's
url: that one animal. - http://127.0.0.1:8000/api/v1/animals/?species=rabbit: only rabbits, with a
countto match. -
The same list from the command line, as one line of JSON:
shellcurl "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
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.