Pagination made easy with Django Rest Framework
Tags: django, python
Published: 2021-01-10 04:00:50

While building any application, the need for fetching a small subset of a large result-set derived from querying a database table is a common occurrence. 

In most cases a user would want quick access to the most recent entries in the result-set, while the older entries are only fetched when the user scrolls up/down or selects the next page of the table. 

Pagination is therefore core to any application but there are several ways to implement it using Django. You could go with the Paginator class that comes out of the box if all you need is the bare minimum, but if you are looking for a more feature rich solution, Django Rest Framework is the way to go.

## How pagination should work

Any type of pagination can be implemented by following a simple API response pattern. An API must return these 4 details.

1. A count of the number of entries in the complete result-set.
2. A list of entries which represents one page of the result-set.
3. A URL that can be used to fetch the next page, if the next page exists.
4. A URL that can be used to fetch the previous page if the previous page exists.

Previously I demonstrated [how CRUD applications can be built using DRF](https://www.sankalpjonna.com/learn-django/building-a-django-crud-application-in-minutes) with an apple notes like application as a reference and I believe the same example can be used to demonstrate pagination.

Let's say that there are 10 notes in the database, but I only want to look at the latest 2 notes at a time, the API response for the note listing API should look like this

    {    
        "count": 10,
        "next": "http://localhost:8000/note/all?p=2",
        "previous": null,
        "results": [
            {
                "id": 10,
                "title": "Note 10",
                "content": "content 10",
                "last_udpated_on": "2021-01-09T01:44:33.645706Z",
                "is_active": true,
                "created": "2021-01-09T01:44:33.645745Z"
            },
            {
                "id": 9,
                "title": "Note 9",
                "content": "content 9",
                "last_udpated_on": "2021-01-09T01:44:29.487257Z",
                "is_active": true,
                "created": "2021-01-09T01:44:29.487295Z"
            }
        ]
    }

This can be achieved using DRF with 2 lines of code.

## Paginating an API response

The quickest way is to set up pagination globally and apply it on all the APIs where a query set is returned as a response.

First make sure you have Django Rest Framework installed in your application, then add these lines of code to **settings.py**

    REST_FRAMEWORK = {
        'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
        'PAGE_SIZE': 2,
    }

That's it, we're done! You will now notice that all the APIs that return a query set as a response have automatically become paginated. If this solves your problem then great, but if you want to dig deeper into the rich solutions offered by DRF, please keep reading.

## Overriding pagination for each API view

If you would like to override individual views with their own pagination settings, this can be done by writing a custom paginator which inherits from any of the existing paginators that DRF provides.

You can then include a **pagination\_class in** your API view as shown below.

    from django.shortcuts import render, get_object_or_404
    
    from rest_framework.viewsets import ModelViewSet
    from .models import Note
    from .serializers import NoteSerializer
    from .paginations import CustomPagination
    # Create your views here.
    
    
    class NoteViewSet(ModelViewSet):
        serializer_class = NoteSerializer
        pagination_class = CustomPagination
    
        def get_object(self):
            return get_object_or_404(Note, id=self.request.query_params.get("id"))
    
        def get_queryset(self):
            return Note.objects.filter(is_active=True).order_by('-last_udpated_on')
    
        def perform_destroy(self, instance):
            instance.is_active = False
            instance.save()

To create a custom pagination class, first create a paginations.py file in your app folder and create a class that inherits from an existing DRF paginator.

    from rest_framework import pagination
    
    class CustomPagination(pagination.PageNumberPagination):
        page_size = 2
        page_size_query_param = 'page_size'
        max_page_size = 50
        page_query_param = 'p'

The parameters given in this custom paginator class will enable you to provide a **page\_size** query parameter to determine the size of each page. If this parameter exists, it will override the default page size of 2. The page number itself would be indicated using the query parameter **p.** 

Therefore, calling the URL **http://localhost:8000/note/all?p=2&page_size=3** will now result in this response:

    {
        "count": 10,
        "next": "http://localhost:8000/note/all?p=3&page_size=3",
        "previous": "http://localhost:8000/note/all?page_size=3",
        "results": [
            {
                "id": 7,
                "title": "Note 7",
                "content": "content 7",
                "last_udpated_on": "2021-01-09T01:44:21.560544Z",
                "is_active": true,
                "created": "2021-01-09T01:44:21.560582Z"
            },
            {
                "id": 6,
                "title": "Note 6",
                "content": "content 6",
                "last_udpated_on": "2021-01-09T01:44:17.883444Z",
                "is_active": true,
                "created": "2021-01-09T01:44:17.883483Z"
            },
            {
                "id": 5,
                "title": "Note 5",
                "content": "content 5",
                "last_udpated_on": "2021-01-09T01:44:13.344068Z",
                "is_active": true,
                "created": "2021-01-09T01:44:13.344107Z"
            }
        ]
    }

This is because of setting a page size of 3 and fetching the second page in the result-set.

## Types of paginations offered by DRF

DRF offers 3 types of paginations that you can choose from based on your use case. You can also set any of these types as the default pagination class in settings.py

* Page number pagination
* Limit and offset pagination
* Cursor pagination

## Page number pagination

This type of pagination is to used to fetch any arbitrary page from the result-set. How it should be used has already been demonstrated in the above example.

## Limit and offset pagination

Simulates a database query where you retrieve part of a result-set from a table by providing a limit and an offset. Limit will constrain the maximum rows returned in the query while offset represents the starting position of the rows with reference to the complete size of the result-set.

    from rest_framework import pagination
    
    class CustomPagination(pagination.LimitOffsetPagination):
        default_limit = 2
        limit_query_param = 'l'
        offset_query_param = 'o'
        max_limit = 50

The **limit\_query\_param** and **offset\_query\_param** will indicate the names of the query params to be used for limit and offset. You can set a **default\_limit** if no query parameter is provided and a **max\_limit** to restrict the max number of rows returned in a page.

If this paginator class is used, the response returned by the URL **http://localhost:8000/note/all?l=3&o=3** would be something like this.

    {
        "count": 10,
        "next": "http://localhost:8000/note/all?l=3&o=6",
        "previous": "http://localhost:8000/note/all?l=3",
        "results": [
            {
                "id": 7,
                "title": "Note 7",
                "content": "content 7",
                "last_udpated_on": "2021-01-09T01:44:21.560544Z",
                "is_active": true,
                "created": "2021-01-09T01:44:21.560582Z"
            },
            {
                "id": 6,
                "title": "Note 6",
                "content": "content 6",
                "last_udpated_on": "2021-01-09T01:44:17.883444Z",
                "is_active": true,
                "created": "2021-01-09T01:44:17.883483Z"
            },
            {
                "id": 5,
                "title": "Note 5",
                "content": "content 5",
                "last_udpated_on": "2021-01-09T01:44:13.344068Z",
                "is_active": true,
                "created": "2021-01-09T01:44:13.344107Z"
            }
        ]
    }

This is because we have specified the limit as 3 which indicates that the page size is 3 and an offset of 3 means that the starting point of this page should be after the first 3 rows in the result-set.

## Cursor pagination

Gives you a database cursor of the exact page you are looking at and therefore it becomes very efficient when querying a large dataset, but the limitation is that the ordering cannot be changed and must be unique.

To order the results, one must use a field that is set once during creation and is relatively unique. When this type of pagination is used, we cannot fetch an arbitrary page and can only move forward or backwards in a result-set.

Also, this pagination will not return a count of the entries because there is no query being run on the entire result-set.

    from rest_framework import pagination
    from rest_framework.response import Response
    from collections import OrderedDict
    
    class CustomPagination(pagination.CursorPagination):
        page_size = 2
        cursor_query_param = 'c'
        ordering = '-id'

**custom\_query\_param** indicates what query parameter to use to represent a cursor and **ordering** indicates the field based on which pagination will be applied. Since this field has to have a fixed order and the values must be unique, I have chosen the primary key in descending as the ordering. 

This is what the response returned by this type of pagination looks like:

    {
        "next": "http://localhost:8000/note/all?c=cD05",
        "previous": null,
        "results": [
            {
                "id": 10,
                "title": "Note 10",
                "content": "content 10",
                "last_udpated_on": "2021-01-09T01:44:33.645706Z",
                "is_active": true,
                "created": "2021-01-09T01:44:33.645745Z"
            },
            {
                "id": 9,
                "title": "Note 9",
                "content": "content 9",
                "last_udpated_on": "2021-01-09T01:44:29.487257Z",
                "is_active": true,
                "created": "2021-01-09T01:44:29.487295Z"
            }
        ]
    }

Clicking on the next URL returns

    {
        "next": "http://localhost:8000/note/all?c=cD03",
        "previous": "http://localhost:8000/note/all?c=cj0xJnA9OA%3D%3D",
        "results": [
            {
                "id": 8,
                "title": "Note 8",
                "content": "content 8",
                "last_udpated_on": "2021-01-09T01:44:25.712231Z",
                "is_active": true,
                "created": "2021-01-09T01:44:25.712268Z"
            },
            {
                "id": 7,
                "title": "Note 7",
                "content": "content 7",
                "last_udpated_on": "2021-01-09T01:44:21.560544Z",
                "is_active": true,
                "created": "2021-01-09T01:44:21.560582Z"
            }
        ]
    }

## Custom pagination

If you prefer to have the count, previous link and next link in the HTTP headers while the data only contains the list of results, you can make these customisations by overriding the **get\_paginated\_response** method in the pagination class that you inherit from any of the above 3 mentioned types of paginations. 

    from rest_framework import pagination
    from rest_framework.response import Response
    from collections import OrderedDict
    
    class CustomPagination(pagination.PageNumberPagination):
        page_size = 2
        page_size_query_param = 'page_size'
        max_page_size = 50
        page_query_param = 'p'
    
        def get_paginated_response(self, data):
            response = Response(data)
            response['count'] = self.page.paginator.count
            response['next'] = self.get_next_link()
            response['previous'] = self.get_previous_link()
            return response

You will now see the links in headers and only the actual data in the response

    HTTP 200 OK
    Allow: GET, HEAD, OPTIONS
    Content-Type: application/json
    Vary: Accept
    count: 10
    next: http://localhost:8000/note/all?p=2
    previous: None
    
    [
        {
            "id": 10,
            "title": "Note 10",
            "content": "content 10",
            "last_udpated_on": "2021-01-09T01:44:33.645706Z",
            "is_active": true,
            "created": "2021-01-09T01:44:33.645745Z"
        },
        {
            "id": 9,
            "title": "Note 9",
            "content": "content 9",
            "last_udpated_on": "2021-01-09T01:44:29.487257Z",
            "is_active": true,
            "created": "2021-01-09T01:44:29.487295Z"
        }
    ]
## Closing notes

It takes a very low amount of code to paginate any list API using Django Rest Framework and therefore I would highly recommend using it over the default paginator class that Django provides out of the box.

DRF itself inherits from the default paginator class so you can save yourself some time.   
[Originally posted on my blog](https://www.sankalpjonna.com/learn-django/pagination-made-easy-with-django-rest-framework)