Pagination in Django REST Framework

One of the most essential parts of building an efficient REST API is handling large datasets gracefully. As your application grows, returning thousands of records in a single response becomes inefficient, slow, and memory-intensive — both for your server and your clients.

That’s where pagination comes into play.

Django REST Framework (DRF) provides a simple yet powerful way to break large sets of data into smaller, manageable chunks, known as pages. Pagination ensures your API responses stay fast, consistent, and user-friendly, no matter how large your database becomes.

In this comprehensive post, we’ll explore everything about pagination in DRF — from the basics to advanced customization techniques.

1. What is Pagination?

Pagination is the process of dividing content into discrete pages.

In the context of APIs, pagination means limiting how much data you send in a single response. Instead of sending thousands of records at once, the API sends only a portion — like 10, 20, or 50 records per request — and includes links to retrieve the next or previous pages.

For example:
If your API returns a list of books, instead of sending all 10,000 books at once, you can return only 10 per page.

Example API Response with Pagination:

{
"count": 50,
"next": "http://localhost:8000/books/?page=2",
"previous": null,
"results": [
    {"id": 1, "title": "Book 1"},
    {"id": 2, "title": "Book 2"},
    ...
]
}

This approach enhances performance, usability, and scalability.


2. Why Pagination is Important

Pagination is not just a technical convenience — it’s an essential performance and design strategy.

a. Performance

Fetching thousands of records in one query can be resource-heavy. Pagination reduces database load and response time.

b. Efficiency

By delivering smaller chunks, users can start interacting with data immediately without waiting for all results.

c. Scalability

As your database grows, pagination keeps the API performant regardless of dataset size.

d. Usability

Clients (like mobile apps or front-end dashboards) can easily navigate through pages using links provided in the API response.

e. Memory Management

Pagination ensures that servers and clients do not run out of memory while processing large responses.


3. Default Pagination in Django REST Framework

DRF provides several built-in pagination styles. You can choose the one that best fits your API design.

The most commonly used is PageNumberPagination, which divides results into pages identified by page numbers (1, 2, 3, etc.).

Step 1: Basic Configuration

In your settings.py, add the following:

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

This configuration ensures that every API view using pagination will return 10 records per page by default.


4. How PageNumberPagination Works

PageNumberPagination uses a query parameter called page to determine which page to display.

Example:

GET /books/?page=1
GET /books/?page=2

Example Response:

{
"count": 50,
"next": "http://localhost:8000/books/?page=2",
"previous": null,
"results": [
    {"id": 1, "title": "Book One"},
    {"id": 2, "title": "Book Two"},
    ...
]
}

Keys Explained:

  • count: Total number of objects.
  • next: Link to the next page (null if it doesn’t exist).
  • previous: Link to the previous page (null if on the first page).
  • results: The actual paginated data.

5. Creating a Custom Pagination Class

Sometimes you might need to customize pagination behavior — like changing parameter names, page size limits, or the response structure.

Example Custom Pagination Class

from rest_framework.pagination import PageNumberPagination

class CustomPagination(PageNumberPagination):
page_size = 5
page_size_query_param = 'size'
max_page_size = 50
page_query_param = 'p'

Explanation:

  • page_size: Default number of items per page.
  • page_size_query_param: Allows clients to control page size dynamically using a query parameter (?size=20).
  • max_page_size: The upper limit for page size.
  • page_query_param: Custom name for the page parameter (e.g., ?p=2 instead of ?page=2).

To use it globally:

REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'myapp.pagination.CustomPagination',
'PAGE_SIZE': 10,
}

Or in a specific view:

class BookListView(generics.ListAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
pagination_class = CustomPagination

6. LimitOffsetPagination

LimitOffsetPagination is another style where the client controls how many items to retrieve (limit) and where to start (offset).

Example Configuration

REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 10
}

Example API Requests

GET /books/?limit=10&offset=0
GET /books/?limit=10&offset=20

Example Response

{
"count": 100,
"next": "http://localhost:8000/books/?limit=10&offset=10",
"previous": null,
"results": [...]
}

This pagination style is useful when clients need precise control over how much data to load and from where.


7. CursorPagination

CursorPagination is ideal for large datasets where records are constantly being added or modified. Instead of using page numbers or offsets, it uses encoded cursors for navigation.

This ensures consistent ordering and avoids issues caused by changing datasets.

Example Configuration

REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 10
}

Example View

from rest_framework.pagination import CursorPagination

class BookCursorPagination(CursorPagination):
page_size = 10
ordering = '-created_at'

Example Response

{
"next": "http://localhost:8000/books/?cursor=cD0yMDI1LTEwLTE1KzEyJTNBMDA",
"previous": null,
"results": [...]
}

Advantages:

  • More stable for large or frequently updated datasets.
  • Prevents duplicate or missing records during pagination.
  • Efficient for infinite scroll applications.

8. Per-View Pagination

While global pagination settings affect all API views, you can apply different pagination styles to specific views.

Example

from rest_framework import generics
from .models import Book
from .serializers import BookSerializer
from .pagination import CustomPagination

class BookListView(generics.ListAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
pagination_class = CustomPagination

This way, only BookListView will use CustomPagination, while other views can use default settings.


9. Disabling Pagination for a View

Sometimes you might not want pagination for specific endpoints — for example, if the dataset is small or you want to return all results.

Example

class AuthorListView(generics.ListAPIView):
queryset = Author.objects.all()
serializer_class = AuthorSerializer
pagination_class = None

By setting pagination_class = None, the response will return the full list of objects without pagination.


10. Advanced Customization of Pagination Response

DRF lets you modify the pagination response structure if you want to match a specific frontend format or third-party API convention.

Example: Custom Response Format

from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response

class CustomResponsePagination(PageNumberPagination):
page_size = 10
def get_paginated_response(self, data):
    return Response({
        'total_records': self.page.paginator.count,
        'total_pages': self.page.paginator.num_pages,
        'current_page': self.page.number,
        'next_page': self.get_next_link(),
        'previous_page': self.get_previous_link(),
        'records': data
    })

This will produce a response like:

{
"total_records": 120,
"total_pages": 12,
"current_page": 3,
"next_page": "http://localhost:8000/books/?page=4",
"previous_page": "http://localhost:8000/books/?page=2",
"records": [...]
}

This structure is much more user-friendly and works well with front-end frameworks.


11. Combining Filtering, Ordering, and Pagination

Pagination often works together with filtering and ordering.

Example View with All Three

from rest_framework import generics, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import Book
from .serializers import BookSerializer
from .pagination import CustomPagination

class BookListView(generics.ListAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['genre', 'author']
ordering_fields = ['title', 'published_date']

Now you can paginate, filter, and sort simultaneously:

GET /books/?genre=fiction&ordering=title&page=2

12. Pagination with ViewSets and Routers

Pagination works seamlessly with ViewSets, just like with generic views.

Example

from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
from .pagination import CustomPagination

class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
pagination_class = CustomPagination

Now, requests to /books/ will be automatically paginated.


13. Pagination Parameters in the Client Request

Clients can use query parameters to control pagination.

  • page (for PageNumberPagination): /books/?page=3
  • limit and offset (for LimitOffsetPagination): /books/?limit=20&offset=40
  • cursor (for CursorPagination): /books/?cursor=cD0yMDI1LTEwLTE1KzEyJTNBMDA

If you allow dynamic page_size:

/books/?size=5

This gives flexibility to API consumers.


14. Performance Optimization Tips

Pagination improves performance, but you can go further with optimization:

  1. Use select_related() and prefetch_related()
    Reduce database queries for related models. queryset = Book.objects.select_related('author').all()
  2. Index Database Fields
    Ensure paginated fields (like id, created_at) are indexed.
  3. Use CursorPagination for Large Datasets
    Prevents skipped or duplicate results in real-time data.
  4. Cache Paginated Results
    Store paginated responses temporarily to improve repeated requests.
  5. Limit Client-Controlled Page Sizes
    Prevent abuse by setting max_page_size.

15. Testing Pagination

Testing ensures your pagination behaves as expected.

Example Test Case

from rest_framework.test import APITestCase
from django.urls import reverse
from .models import Book

class BookPaginationTest(APITestCase):
def setUp(self):
    for i in range(30):
        Book.objects.create(title=f"Book {i+1}")
def test_first_page(self):
    response = self.client.get(reverse('book-list'))
    self.assertEqual(response.status_code, 200)
    self.assertEqual(len(response.data['results']), 10)

This confirms that the first page returns exactly 10 results (based on default pagination).


16. Real-World Example: Building Paginated API for a Blog

Let’s imagine a blog application with thousands of posts.

Step 1: Create Model

class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
published_date = models.DateTimeField(auto_now_add=True)

Step 2: Create Serializer

class PostSerializer(serializers.ModelSerializer):
class Meta:
    model = Post
    fields = '__all__'

Step 3: Apply Pagination

class PostPagination(PageNumberPagination):
page_size = 5
page_size_query_param = 'size'
max_page_size = 20

Step 4: Create View

class PostListView(generics.ListAPIView):
queryset = Post.objects.all().order_by('-published_date')
serializer_class = PostSerializer
pagination_class = PostPagination

Result

Now, when you visit /posts/?page=1, you get:

{
"count": 100,
"next": "http://localhost:8000/posts/?page=2",
"previous": null,
"results": [...]
}

A simple, efficient, paginated blog API.


17. Pagination for Infinite Scroll Interfaces

Modern UIs often use infinite scrolling, where more data loads automatically as the user scrolls.

For such use cases, CursorPagination is ideal because:

  • It prevents duplicates when new records are added.
  • It provides consistent results even as the dataset changes.

Clients can keep calling the next cursor URL to fetch more results dynamically.


18. Common Mistakes to Avoid

  1. Not Setting PAGE_SIZE
    Without a page size, DRF may return all records — which defeats pagination.
  2. Using Large Page Sizes
    Keep it reasonable (10–50). Very large page sizes slow responses.
  3. Not Handling Empty Pages
    Always ensure the frontend checks for null next values.
  4. Ignoring Pagination in Testing
    Paginated responses can behave differently; always write pagination tests.
  5. Returning Unnecessary Metadata
    Keep response clean and meaningful.

19. Best Practices

  • Always include pagination for list endpoints.
  • Use CursorPagination for live data feeds.
  • Allow client-defined page sizes (with limits).
  • Customize pagination structure for better frontend integration.
  • Document pagination behavior in your API docs (page parameters, size limits, etc.).

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *