Pagination in DRF: PageNumberPagination and beyond
/tasks/ has returned every task in one response since part 4 — fine with five test rows, a real problem at fifty thousand. DRF's pagination classes fix this globally, without touching TaskViewSet at all.
Setting a default globally
# taskapi/settings.py
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}REST_FRAMEWORK is DRF's project-wide settings dict — everything here applies to every view and viewset by default, unless a specific class overrides it. Setting pagination here, once, is what covers TaskViewSet and every future viewset in this project without adding anything to views.py.
What the response looks like now
curl http://127.0.0.1:8000/api/tasks/{
"count": 47,
"next": "http://127.0.0.1:8000/api/tasks/?page=2",
"previous": null,
"results": [ { "id": 1, "title": "..." }, "... 9 more ..." ]
}The response shape changed: a plain array of tasks became an object with count (total across every page, not just this one), next/previous (full URLs, ready to fetch directly, or null at either end), and results (the actual page of tasks). A frontend consuming this API needs to read results instead of treating the whole response as the list — worth knowing before it causes a confusing .map is not a function error on the client side.
Other pagination styles
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",/tasks/?limit=5&offset=10LimitOffsetPagination trades page numbers for direct control over exactly how many rows and from which starting point — useful for a client that wants to fetch in custom-sized chunks rather than DRF's fixed PAGE_SIZE. CursorPagination is a third option, better suited to a feed that's being actively written to while being paginated (a ?page=2 request can skip or repeat rows if new items were inserted between requests; cursor-based pagination is built specifically to avoid that).
Per-view overrides
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
pagination_class = None # opts this specific viewset out entirelySetting pagination_class directly on a view or viewset overrides the global default — None disables pagination just for that one endpoint, worth doing deliberately for something like a small, fixed lookup table that will never grow large enough to need it.
An unpaginated list endpoint isn't just a performance inconvenience once real data volume shows up — it's a genuine denial-of-service risk. A single request that forces the database to fetch and serialize every row in a large table is exactly the kind of endpoint that takes a server down under moderate, even accidental, load.
Next: letting a client narrow down that list — filtering, searching, and ordering, instead of always fetching every task and filtering client-side.