Implement and Verify DRF PageNumberPagination with a Custom Page Size
Step‑by‑step guide to set up a custom PageNumberPagination in Django REST Framework, test it, and validate count, next, previous, and results metadata.
15 Aug 2026, 16:14 UTC

What You’ll Achieve
By the end of this guide you’ll have a ListAPIView that returns paginated JSON with count, next, previous, and results keys. The page size will be hard‑coded to 5 items per page, but you’ll also see how to expose a page_size query parameter if you choose to let clients override it. Finally you’ll run a quick unit test to confirm the pagination metadata is present.
Prerequisites
- Python 3.10+ and Django 5.0+ installed.
- Basic familiarity with Django models, serializers, and views.
- Access to a terminal where you can run
python manage.pycommands.
Step 1 – Create a Minimal Django Project
# In your shell
mkdir drf_pagination_demo && cd drf_pagination_demo
python -m venv venv
source venv/bin/activate
pip install django djangorestframework
# Start project and app
django-admin startproject demo .
python manage.py startapp items
Add rest_framework and items to INSTALLED_APPS in demo/settings.py:
INSTALLED_APPS = [
...
'rest_framework',
'items',
]
Step 2 – Define a Simple Model
Create a Item model with a single name field in items/models.py:
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
Run migrations:
python manage.py makemigrations items
python manage.py migrate
Step 3 – Add a Serializer
In items/serializers.py:
from rest_framework import serializers
from .models import Item
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ['id', 'name']
Step 4 – Create a Custom Pagination Class
DRF ships with PageNumberPagination. We’ll subclass it to set a default page size of 5 and optionally allow a page_size query param.
In items/pagination.py:
from rest_framework.pagination import PageNumberPagination
class FiveItemPagination(PageNumberPagination):
# Hard‑code 5 items per page
page_size = 5
# Allow clients to override via ?page_size=10 (if you want to expose this)
page_size_query_param = 'page_size'
# Upper limit to guard against very large requests
max_page_size = 100
Step 5 – Wire the View
In items/views.py:
from rest_framework.generics import ListAPIView
from .models import Item
from .serializers import ItemSerializer
from .pagination import FiveItemPagination
class ItemListView(ListAPIView):
queryset = Item.objects.all().order_by('id')
serializer_class = ItemSerializer
pagination_class = FiveItemPagination
Register the view in items/urls.py and include it in the project’s urls.py:
# items/urls.py
from django.urls import path
from .views import ItemListView
urlpatterns = [
path('api/items/', ItemListView.as_view(), name='item-list'),
]
# demo/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('items.urls')),
]
Step 6 – Seed Some Data
Open a Django shell and create 12 items so we can see pagination in action:
python manage.py shell
>>> from items.models import Item
>>> for i in range(12):
... Item.objects.create(name=f'Item {i+1}')
...
>>> exit()
Step 7 – Test the Endpoint Manually
Run the development server:
python manage.py runserver
Open a browser or use curl:
curl http://127.0.0.1:8000/api/items/?page=2
Expected JSON snippet:
{
"count": 12,
"next": "http://127.0.0.1:8000/api/items/?page=3",
"previous": "http://127.0.0.1:8000/api/items/?page=1",
"results": [
{"id": 6, "name": "Item 6"},
...
{"id": 10, "name": "Item 10"}
]
}
Key checks:
countequals total items (12).- Exactly 5 objects in
results. - Correct
nextandpreviousURLs.
Edge case: request a page beyond the last page:
curl http://127.0.0.1:8000/api/items/?page=999
Result:
{
"count": 12,
"next": null,
"previous": "http://127.0.0.1:8000/api/items/?page=3",
"results": []
}
Step 8 – Disable Pagination Globally (Optional)
If you later want to turn off pagination across the project, set the following in demo/settings.py:
REST_FRAMEWORK = {
'PAGINATION_CLASS': None,
}
Now the same endpoint will return the full list without count, next, or previous keys.
Step 9 – Write a Unit Test
In items/tests.py add:
from django.test import TestCase
from rest_framework.test import APIClient
from .models import Item
class PaginationTest(TestCase):
def setUp(self):
for i in range(12):
Item.objects.create(name=f'Item {i+1}')
self.client = APIClient()
def test_pagination_metadata(self):
response = self.client.get('/api/items/?page=1')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn('count', data)
self.assertIn('next', data)
self.assertIn('previous', data)
self.assertIn('results', data)
self.assertEqual(len(data['results']), 5)
self.assertEqual(data['count'], 12)
Run the test:
python manage.py test items
If all assertions pass, your pagination is correctly wired.
Step 10 – Recover from Common Issues
- Missing pagination metadata: Verify that
pagination_classis set on the view and that you didn’t overridepaginate_querysetwithout callingsuper(). - Large responses: If you allow
page_sizewithout amax_page_size, clients could request thousands of items. Keepmax_page_sizesensible. - Inconsistent count after filtering: When combining pagination with filters, DRF automatically recalculates
count. If you see mismatches, ensure your filter backend runs before pagination.
Conclusion
With a single custom pagination class you can control how many items appear per page, expose a query param if desired, and guarantee the presence of useful metadata. The quick test case gives you confidence that future refactors won’t break the contract your front‑end relies on.
Diagram
| Component |
|---|
| Model → Serializer → View → Pagination → Response |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.