Skip to content

Viewsets, Routers & the Browsable API

We need URLs that return the animals as JSON: one for the list, and one for each animal. Our pages do the same two jobs with ListView and DetailView, and DRF has a single class that does both for an API: a viewset.

Our app has both pages and an API. We'll keep the API's views in animals/api.py and its URL patterns in animals/api_urls.py, while the pages' files are in views.py and urls.py.

A Read-Only Viewset

Create animals/api.py:

animals/api.py
from rest_framework import viewsets

from . import queries
from .serializers import AnimalSerializer


class AnimalViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = queries.all_animals()
    serializer_class = AnimalSerializer

ReadOnlyModelViewSet provides two actions:

  • list returns the rows of queryset.
  • retrieve returns one row by its primary key, or a 404 when there isn't one.

As with ListView and DetailView, we configure code that's already written. queryset says which rows, and serializer_class turns each one into the values DRF renders as JSON, the job a template does for a page.

queries.all_animals() returns every animal, newest arrivals first. Unlike the site's list page, the API includes adopted animals.

Routers

A viewset's actions aren't tied to HTTP methods or URLs yet. A router connects them. It binds each action to an HTTP method, and writes a URL pattern for each. Create animals/api_urls.py:

animals/api_urls.py
from rest_framework.routers import DefaultRouter

from . import api

router = DefaultRouter()
router.register("animals", api.AnimalViewSet)

urlpatterns = router.urls

register() takes a URL prefix and a viewset. The router writes two URLs and names each after the model in the viewset's queryset:

URL Name
animals/ animal-list
animals/<pk>/ animal-detail

router.urls is the list of patterns it wrote, so it can be the module's urlpatterns.

DefaultRouter also writes an API root that links to each list. It's at the address where we include the router's patterns.

Then include the router's patterns in config/urls.py, below the admin's pattern:

config/urls.py
urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("animals.api_urls")),

The v1 lets a changed API go live at v2 while existing clients keep using v1.

The Browsable API

A viewset's actions return a DRF Response, which holds the serializer's data, not a rendered string. DRF chooses the format from the request's Accept header: JSON for a program, and an HTML page for a browser. Choosing a format from the request's headers is called content negotiation.

If the server isn't running, start it with uv run manage.py runserver. Open http://127.0.0.1:8000/api/v1/. A browser sends Accept: text/html, so DRF renders the browsable API, with the response's status, headers, and JSON.

  • This is the API root. Its link goes to the animals list.
  • In the list, fields with choices come out as their stored values, such as "cat", not their labels.
  • Add an animal's id to the address, as in /api/v1/animals/<id>/, for that one animal.

The GET button's menu has a json option. It loads the same address with ?format=json and shows the JSON a program receives.

curl sends Accept: */*, which accepts any format, so DRF returns JSON, its default format:

shell
curl http://127.0.0.1:8000/api/v1/animals/

The response is one long line: a list of animals, each with the fields from our serializer.

Keep the trailing slash

Without it, as in /api/v1/animals, Django responds with a 301 redirect to the address with the slash. curl doesn't follow redirects unless we add -L, so it prints nothing.

Who Can Call the API

Anyone can call our API without logging in: DRF's default permission class, AllowAny, lets every request through. Our API stays open. A production API would require authentication for anything that isn't public, and limit how many requests a client can make.