Implementing a custom pagination in Django Rest Framework (DRF) using ListAPIView and APIView

When user-generated content becomes big, it's best practice to paginate returning content. Pagination is used to retrieve a large amount of data in chunks. 

In this article, you will learn how to customize one of the pagination approach, which is implemented by PageNumberPagination class in Django Rest Framework. We will cover enabling pagination in ListAPIView and APIView.

Let's suppose, we are building a backend for a blog website and it has a model Post with the following fields.

class Post(models.Model):
    title = models.CharField(max_length=256)
    slug = models.SlugField(unique=True)
    text = models.TextField()
    image = models.ImageField(upload_to='blog/%Y-%m/', blank=True, null=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    is_active = models.BooleanField(default=True)
    created_dt = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

Create a module called paginators.py inside the same application directory with the following content: 

from rest_framework.pagination import PageNumberPagination


class CustomPagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = 'page_size'
    max_page_size = 20
    page_query_param = 'page'

Our custom paginator class inherits from PageNumberPagination, so we can customize it as we want.  

  • page_size = 5 - Each page will contain 5 records
  • page_query_param = 'page' means that query param is called “page” which is used to tell the DRF which page we want to read: /api/v1/posts?page=2
  • page_size_query_param = 'page_size' - also we can control how many records to fetch per page by including query param page_size:  /api/v1/posts?page=2&page_size=3. In this case, page_size = 5 will be overridden by query parameter value (?page_size=3). So it returns 3 records on each page. But it does not return records more than max_page_size value, which is 20 in our case.
  • max_page_size = 20 : maximum allowed records count it can return per page.

 

Enable pagination in generics.ListAPIView

Now, in module views.py import CustomPagination class and assign it to generics.ListAPIView  class pagination_class attribute

from rest_framework import generics

from . import serializers
from .paginators import CustomPagination
from . import models


class PostListView(generics.ListAPIView):
    authentication_classes = ()
    permission_classes = ()
    serializer_class = serializers.BlogSerializer
    pagination_class = CustomPagination
    queryset = models.Post.objects.all().order_by('-created_dt')

 

Enable pagination in APIView

class PostListView(APIView):
    authentication_classes = ()
    permission_classes = ()
    serializer_class = serializers.BlogSerializer
    pagination_class = CustomPagination
    queryset = models.Post.objects.all().order_by('-created_dt')

    @property
    def paginator(self):
        """The paginator instance associated with the view, or `None`."""
        if not hasattr(self, '_paginator'):
            if self.pagination_class is None:
                self._paginator = None
            else:
                self._paginator = self.pagination_class()
        return self._paginator

    def paginate_queryset(self, queryset):
        """Return a single page of results, or `None` if pagination is disabled."""
        if self.paginator is None:
            return None
        return self.paginator.paginate_queryset(queryset, self.request, view=self)

    def get_paginated_response(self, data):
        """Return a paginated style `Response` object for the given output data."""
        assert self.paginator is not None
        return self.paginator.get_paginated_response(data)

    def get(self, request):
        queryset = self.queryset.filter(is_active=True).order_by('-created_dt')
        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.serializer_class(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.serializer_class(queryset, many=True)
        return Response(serializer.data)

 

Create a module serializers.py and paste the following code into it:

from rest_framework import serializers

from . import models


class BlogSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Post
        fields = (
            'id',
            'title',
            'text',
            'image',
            'author',
            'is_active',
            'created_dt',
        )

Then register post list endpoint view in blog/urls.py:

from django.urls import path
from . import views

app_name = 'api_blog'

urlpatterns = [
    path('', views.PostListView.as_view(), name='post-list'),

]

In the root urls.py, include the blog application routes.

urlpatterns = [
    path('admin/', admin.site.urls),
    
...
    # API
    path('api/v1/posts', include('blog.urls', namespace='api_blog')),
]

 

Now, you can use any REST API client to test our endpoint. Send a request to http://127.0.0.1:8000/api/v1/posts and http://127.0.0.1:8000/api/v1/posts?page=2