~/TechPurAI
~/tutorials/django-rest-framework/pagination
intermediate·part 13 of 22·2 min read

Pagination in DRF: PageNumberPagination and beyond

Updated Aug 16, 2026Python · Django

/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

python
# 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

bash
curl http://127.0.0.1:8000/api/tasks/
json
{
  "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

python
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
text
/tasks/?limit=5&offset=10

LimitOffsetPagination 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

python
class TaskViewSet(ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    pagination_class = None   # opts this specific viewset out entirely

Setting 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.

Why this matters beyond convenience

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.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous12. Nested serializers and relationshipsnext →14. Filtering, searching, and ordering results