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=2instead 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:
- Use select_related() and prefetch_related()
Reduce database queries for related models.queryset = Book.objects.select_related('author').all() - Index Database Fields
Ensure paginated fields (likeid,created_at) are indexed. - Use CursorPagination for Large Datasets
Prevents skipped or duplicate results in real-time data. - Cache Paginated Results
Store paginated responses temporarily to improve repeated requests. - Limit Client-Controlled Page Sizes
Prevent abuse by settingmax_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
- Not Setting PAGE_SIZE
Without a page size, DRF may return all records — which defeats pagination. - Using Large Page Sizes
Keep it reasonable (10–50). Very large page sizes slow responses. - Not Handling Empty Pages
Always ensure the frontend checks for nullnextvalues. - Ignoring Pagination in Testing
Paginated responses can behave differently; always write pagination tests. - Returning Unnecessary Metadata
Keep response clean and meaningful.
19. Best Practices
- Always include pagination for list endpoints.
- Use
CursorPaginationfor 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.).
Leave a Reply