ViewSets, routers and pagination
Writing a separate view for list, retrieve, create, update and delete of the same resource is repetitive — five views that differ only in which action they perform. DRF's ViewSets bundle all of them into one class, and routers generate the URLs automatically, so a full CRUD API for a resource becomes a few lines. Add pagination and your API scales to large datasets without sending everything at once. This lesson covers all three.
ModelViewSet: full CRUD in one class
A ModelViewSet provides list, retrieve, create, update, partial-update and destroy — the entire REST
lifecycle for a model — from a queryset and a serializer:
# patients/api.py
from rest_framework import viewsets
from .models import Patient
from .serializers import PatientSerializer
class PatientViewSet(viewsets.ModelViewSet):
queryset = Patient.objects.all().order_by("name")
serializer_class = PatientSerializer
That is a complete CRUD API for patients. Verified: it serves GET /api/patients/ (list, 200),
GET /api/patients/{id}/ (retrieve), POST /api/patients/ (create, 201), and the update/delete actions —
all from those two lines. The ModelViewSet maps HTTP methods to actions for you: GET on the collection
lists, GET on an item retrieves, POST creates, PUT/PATCH update, DELETE destroys. You wrote no
per-action code.
Always set an explicit order_by on the queryset — an unordered queryset can return rows in different
orders across requests, which breaks pagination (below).
Routers: URLs generated for you
A ViewSet has no URLs by itself — a router generates the standard REST URLs from it:
# patients/api_urls.py
from rest_framework.routers import DefaultRouter
from .api import PatientViewSet
router = DefaultRouter()
router.register(r"patients", PatientViewSet, basename="api-patient")
urlpatterns = router.urls
# config/urls.py
path("api/", include("patients.api_urls")),
router.register("patients", PatientViewSet) produces the whole set of URLs — /api/patients/ for the
collection and /api/patients/{id}/ for an item — each wired to the right ViewSet action. Verified: the
registered routes serve list, retrieve, create and the rest under /api/. The router also generates a
handy API root view listing your registered endpoints. This is the pay-off: a ViewSet plus a router is a
complete, conventionally-routed CRUD API with no hand-written URL patterns or per-action views.
When a ViewSet is too much: choose the right granularity
ModelViewSet gives you all actions. Sometimes you want fewer:
ReadOnlyModelViewSet— only list and retrieve (no create/update/delete). Right for data a client may read but not change (a public list of test types, say).- A ViewSet with specific mixins — compose exactly the actions you want.
- Custom actions — add an extra endpoint with the
@actiondecorator:
from rest_framework.decorators import action
from rest_framework.response import Response
class PatientViewSet(viewsets.ModelViewSet):
# ...
@action(detail=True, methods=["get"])
def appointments(self, request, pk=None):
patient = self.get_object()
data = AppointmentSerializer(patient.appointments.all(), many=True).data
return Response(data)
@action(detail=True) adds /api/patients/{id}/appointments/ — an extra endpoint beyond CRUD, routed
automatically. Use detail=False for a collection-level action. This is how you extend a ViewSet with the
non-CRUD endpoints a real API needs without abandoning the ViewSet's convenience.
Pagination: never return everything
A list endpoint that returns every row is a problem waiting for scale — ten thousand patients in one response is slow, memory-hungry, and unusable on a mobile client. DRF paginates automatically once configured:
# settings.py
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
Now every list endpoint returns one page and the metadata to fetch the rest:
{
"count": 2450,
"next": "http://.../api/patients/?page=2",
"previous": null,
"results": [ ... 20 patients ... ]
}
The client reads results for this page and follows next for more. PageNumberPagination is the simplest
(?page=2); LimitOffsetPagination (?limit=20&offset=40) and cursor pagination are alternatives for
different needs. The key discipline: a list endpoint should be paginated from the start. Set a global
PAGE_SIZE and every list is bounded — retrofitting pagination after clients depend on unpaginated
responses is a breaking change, so do it now.
Check your work
What ModelViewSet provides. Full CRUD — list, retrieve, create, update, partial-update, destroy —
from a queryset and serializer_class, mapping HTTP methods to actions. Verified: list (200), retrieve,
create (201) with no per-action code.
What a router does. Generates the standard REST URLs from a registered ViewSet
(/api/patients/, /api/patients/{id}/) plus an API root — no hand-written URL patterns.
Choosing granularity. ReadOnlyModelViewSet for read-only resources; mixins for specific actions;
@action to add non-CRUD endpoints (verified pattern: /patients/{id}/appointments/).
Why order the queryset. An unordered queryset returns rows in unstable order, breaking pagination —
always set order_by.
Pagination. Configure DEFAULT_PAGINATION_CLASS + PAGE_SIZE; list responses become
{count, next, previous, results}. Paginate from the start — retrofitting is a breaking change.
Practice
- Replace an
APIViewlist with aModelViewSet; register it with aDefaultRouterand confirm list, retrieve and create all work under/api/. - Visit the router's API root and the browsable endpoints; create and edit a patient through the browsable API.
- Switch to
ReadOnlyModelViewSetand confirmPOSTis now rejected (405) — the resource is read-only. - Add an
@action(detail=True)endpoint returning a patient's appointments; confirm the generated URL works. - Configure
PAGE_SIZE = 2and confirm the list response hascount,next,previous,results; follownextto page 2. - Remove the
order_byfrom the queryset and reason about why pagination could then return inconsistent pages.
Official documentation
- DRF — ViewSets —
ModelViewSet,ReadOnlyModelViewSet,@action. - DRF — Routers — Generating URLs from ViewSets.
- DRF — Pagination — The pagination classes and settings.
Next: filtering, searching and ordering an API.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship