Mastering Custom Pagination in Django Rest Framework
Default pagination in Django Rest Framework is powerful, but often the frontend needs more metadata than the default implementation provides—specifically, the total number of pages.
In this guide, we will build a robust custom pagination class that includes page counts and standardizes the API response structure. We will then implement this in both a ListAPIView and a ModelViewSet.
Prerequisite: The Data Model
First, let's establish the model we are working with. We are using a Task model that tracks completion status and ownership.
models.py
from django.db import models
from users.models import CustomUser
class Task(models.Model):
title = models.CharField(max_length=200)
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
# Relationships
creator = models.ForeignKey(CustomUser, related_name='creator', on_delete=models.SET_NULL, null=True, blank=True)
modifier = models.ForeignKey(CustomUser, related_name='modifier', on_delete=models.SET_NULL, null=True, blank=True)
assign_to = models.ForeignKey(CustomUser, related_name='assign_to', on_delete=models.SET_NULL, null=True, blank=True)
def __str__(self):
return self.title
Step 1: The Serializer
Before we paginate, we need to define how our data is transformed into JSON.
serializers.py
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = '__all__'
Step 2: Creating the Custom Pagination Class
This is the core of our implementation. We will extend PageNumberPagination to override the get_paginated_response method. This allows us to inject total_pages into the response, which is crucial for building UI pagination components (e.g., "Page 1 of 5").
pagination.py
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
from rest_framework import status
class CustomPagination(PageNumberPagination):
# Set the default page size
page_size = 10
# Allow the client to set the page size via a query param (e.g., ?page_size=20)
page_size_query_param = 'page_size'
# Set a maximum limit to prevent server overload
max_page_size = 100
def get_paginated_response(self, data):
final_data = {
"count": self.page.paginator.count,
"next": self.get_next_link(),
"previous": self.get_previous_link(),
"total_pages": self.page.paginator.num_pages, # The custom addition
"results": data,
}
return Response(final_data, status=status.HTTP_200_OK)
Step 3: Implementation in Views
We will now apply this pagination to our views. It is important to explicitly set the pagination_class attribute.
views.py
from rest_framework.generics import ListAPIView
from rest_framework import viewsets
from .models import Task
from .serializers import TaskSerializer
from .pagination import CustomPagination
# Implementation 1: Using ListAPIView
class TaskListAPIView(ListAPIView):
"""
API endpoint that allows tasks to be listed with custom pagination.
"""
queryset = Task.objects.all().order_by('-created_at')
serializer_class = TaskSerializer
pagination_class = CustomPagination # Hooking up the custom class
# Implementation 2: Using ModelViewSet
class TaskViewSet(viewsets.ModelViewSet):
"""
A ViewSet for viewing and editing tasks.
"""
queryset = Task.objects.all().order_by('-created_at')
serializer_class = TaskSerializer
pagination_class = CustomPagination # Hooking up the custom class
Step 4: URL Configuration
Finally, let's expose these views via URLs to test them.
urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskListAPIView, TaskViewSet
# Create a router and register the ViewSet
router = DefaultRouter()
router.register(r'tasks-set', TaskViewSet, basename='task-set')
urlpatterns = [
# Route for the ListAPIView
path('tasks/', TaskListAPIView.as_view(), name='task-list'),
# Route for the ViewSet
path('', include(router.urls)),
]
The Result
When you make a GET request to /api/tasks/, your response will now look like this:
{
"count": 55,
"next": "http://127.0.0.1:8000/api/tasks/?page=2",
"previous": null,
"total_pages": 6,
"results": [
{
"id": 1,
"title": "Fix pagination bug",
"completed": false,
...
},
...
]
}
Pro-Tip: Global Configuration
If you want to apply this pagination style across your entire project without adding it to every view manually, add this to your settings.py:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'path.to.your.pagination.CustomPagination',
}