10. Django REST API: Pagination
Django REST API – Pagination

In this post, we will add pagination to our Django REST API.
Pagination helps control how many results are returned in each response, making the API faster, cleaner, and easier to consume from frontend applications.



 

Why pagination matters
At the moment, our API list endpoints can return all results at once.

For example:

GET /api/tasks/

If the authenticated user only has a few tasks, this is not a problem.

But if the database grows, returning everything in a single response can become inefficient.

10 tasks     → Fine
100 tasks    → Still acceptable
10,000 tasks → Too much data in one response

Pagination solves this by splitting results into smaller pages.

 

What is pagination?
Pagination is the process of dividing a large list of results into smaller groups.

Instead of returning all tasks at once, the API can return only a limited number per page.

/api/tasks/?page=1
/api/tasks/?page=2
/api/tasks/?page=3

This makes API responses smaller and more predictable.

Client requests page 1
API returns first group of results

Client requests page 2
API returns next group of results

 

Pagination in Django REST Framework
Django REST Framework includes several pagination styles.

PageNumberPagination
LimitOffsetPagination
CursorPagination

Each style has a different use case.

PageNumberPagination → Simple page-based navigation
LimitOffsetPagination → Flexible limit and offset queries
CursorPagination → Better for large or frequently changing datasets

For this series, we will start with PageNumberPagination, because it is simple and easy to understand.

 

Adding pagination globally
The simplest way to add pagination is to configure it globally in settings.py.

Open config/config/settings.py and update the REST_FRAMEWORK configuration.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 5,
}

The DEFAULT_PAGINATION_CLASS defines which pagination style will be used.

The PAGE_SIZE defines how many objects should be returned per page.

DEFAULT_PAGINATION_CLASS → PageNumberPagination
PAGE_SIZE                → Number of results per page

In this example, each page will return up to 5 results.

 

Testing pagination
Run the development server:

python manage.py runserver

Then open one of your list endpoints:

Tasks: http://localhost:8000/api/tasks/

If you have more than 5 tasks, the response will be paginated.

To access the second page, use:

Tasks Page 2: http://localhost:8000/api/tasks/?page=2

 

Paginated response structure
A paginated response includes extra information about the result list.

{
    "count": 12,
    "next": "http://localhost:8000/api/tasks/?page=2",
    "previous": null,
    "results": [
        {
            "id": 1,
            "project": 1,
            "title": "Create homepage",
            "description": "Create the first version of the homepage",
            "completed": false,
            "created_at": "2026-05-05T12:00:00Z"
        },
        {
            "id": 2,
            "project": 1,
            "title": "Create contact page",
            "description": "Add a contact form",
            "completed": false,
            "created_at": "2026-05-05T12:15:00Z"
        }
    ]
}

The response now contains metadata and the actual data.

count     → Total number of results
next      → URL for the next page
previous  → URL for the previous page
results   → Current page results

This structure is very useful for frontend applications.

 

Changing page size
The global PAGE_SIZE value controls how many results are returned per page.

'PAGE_SIZE': 5,

If you want to return 10 results per page, change it to:

'PAGE_SIZE': 10,

A smaller page size makes responses lighter.

A larger page size reduces the number of requests needed to load more data.

Smaller page size → Smaller responses, more requests
Larger page size  → Larger responses, fewer requests

For learning, a small value like 5 is useful because it makes pagination easier to test.

 

Combining pagination with filtering
Pagination works together with filtering, search, and ordering.

For example, we can request only incomplete tasks and paginate the results.

/api/tasks/?completed=false&page=2

This means:

Filter tasks where completed is false
Return page 2 of those filtered results

We can also combine search and ordering.

/api/tasks/?search=homepage&ordering=-created_at&page=1

This is one of the reasons pagination is so important in real APIs.

 

PageNumberPagination
PageNumberPagination is the simplest pagination style.

It uses a page number in the URL.

/api/tasks/?page=1
/api/tasks/?page=2
/api/tasks/?page=3

This is easy to understand and works well for many applications.

Admin panels
Dashboards
Simple lists
Learning projects
Small and medium APIs

For this series, this is the best starting point.

 

LimitOffsetPagination
Another common style is LimitOffsetPagination.

Instead of pages, it uses limit and offset.

/api/tasks/?limit=10&offset=0
/api/tasks/?limit=10&offset=10
/api/tasks/?limit=10&offset=20

The limit defines how many results to return.

The offset defines how many results to skip before starting.

limit=10&offset=0   → First 10 results
limit=10&offset=10  → Next 10 results
limit=10&offset=20  → Next 10 results after that

This style is useful when the client wants more control over the result window.

 

CursorPagination
CursorPagination is more advanced.

Instead of page numbers or offsets, it uses a cursor value to move through the result set.

/api/tasks/?cursor=abc123

This is often better for large datasets or data that changes frequently.

Activity feeds
Large datasets
Frequently updated lists
Infinite scroll interfaces

Cursor pagination is powerful, but for a first REST API project, PageNumberPagination is easier to understand.

 

Creating a custom pagination class
Sometimes we want more control over pagination.

For example, we may want the client to choose the page size using a query parameter.

Inside the projects app, create a file called pagination.py.

projects/
    __init__.py
    admin.py
    apps.py
    migrations/
    models.py
    pagination.py
    permissions.py
    serializers.py
    tests.py
    urls.py
    views.py

Add the following class:

from rest_framework.pagination import PageNumberPagination


class StandardResultsSetPagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = 'page_size'
    max_page_size = 50

This allows the client to control the page size using page_size, but only up to a maximum limit.

/api/tasks/?page_size=10
/api/tasks/?page_size=20
/api/tasks/?page_size=100

Because max_page_size is set to 50, the API will not return more than 50 results per page.

 

Using the custom pagination class globally
To use this custom pagination class, update config/config/settings.py.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_PAGINATION_CLASS': 'projects.pagination.StandardResultsSetPagination',
}

With this configuration, all list endpoints will use the custom pagination class by default.

Now the client can request:

/api/tasks/?page_size=10
/api/tasks/?page=2&page_size=10

This gives more flexibility while still keeping a maximum limit.

 

Using pagination per ViewSet
Pagination can also be configured per ViewSet instead of globally.

For example, we can apply the custom pagination class only to tasks.

from .pagination import StandardResultsSetPagination


class TaskViewSet(viewsets.ModelViewSet):
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]
    pagination_class = StandardResultsSetPagination
    filterset_fields = ['project', 'completed', 'created_at']
    search_fields = ['title', 'description']
    ordering_fields = ['title', 'completed', 'created_at']
    ordering = ['-created_at']

    def get_queryset(self):
        return Task.objects.filter(project__owner=self.request.user)

    def perform_create(self, serializer):
        project = serializer.validated_data['project']

        if project.owner != self.request.user:
            raise PermissionDenied('You cannot create tasks for this project.')

        serializer.save()

This is useful when different endpoints need different pagination behaviour.

Projects may use one page size
Tasks may use another page size
Some endpoints may disable pagination

For this series, global pagination is enough, but it is useful to know that per-view configuration is possible.

 

Disabling pagination for one ViewSet
If you want to disable pagination for a specific ViewSet, set pagination_class to None.

class ProjectViewSet(viewsets.ModelViewSet):
    serializer_class = ProjectSerializer
    permission_classes = [IsAuthenticated]
    pagination_class = None

    def get_queryset(self):
        return Project.objects.filter(owner=self.request.user)

This means the projects endpoint would return all project results without pagination.

Use this carefully. For endpoints that can grow a lot, pagination should usually stay enabled.

 

Frontend pagination flow
A frontend application can use the next and previous values from the API response to navigate between pages.

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

The frontend can show buttons such as:

Previous
Next
Page 1
Page 2
Page 3

When the user clicks Next, the frontend requests the URL from the next field.

This keeps the frontend logic simple because the API already provides the navigation links.

 

Pagination and performance
Pagination improves performance because the API does not need to return every result at once.

Smaller JSON responses
Less memory usage
Faster response times
Better frontend loading experience
More predictable API behaviour

However, pagination is not the only performance concern.

As the API grows, database queries, indexes, serializer complexity, and permissions also affect performance.

Pagination is an important first step toward keeping API responses manageable.

 

Recommended approach
For this project, a good pagination setup is:

Use PageNumberPagination
Start with a small page size while learning
Allow page_size as a query parameter if needed
Set a max_page_size to protect the API
Keep pagination enabled for list endpoints

A practical custom class is:

from rest_framework.pagination import PageNumberPagination


class StandardResultsSetPagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = 'page_size'
    max_page_size = 50

And the global setting:

'DEFAULT_PAGINATION_CLASS': 'projects.pagination.StandardResultsSetPagination',

This gives the API a good balance between simplicity and flexibility.

 

What we have learned
In this post, we added pagination to our Django REST API.

Pagination splits large result sets into smaller pages
PageNumberPagination is simple and beginner-friendly
PAGE_SIZE controls the number of results per page
Paginated responses include count, next, previous, and results
Pagination works with filtering, search, and ordering
Custom pagination classes give more control
page_size_query_param lets clients request different page sizes
max_page_size protects the API from very large responses

Our API is now more scalable and easier for frontend applications to consume.

 

What comes next?
In the next post, we will focus on validation and error handling.

We will learn how to validate incoming data, return useful error messages, and make the API safer and more predictable.

Django REST API: Validation and Error Handling