Skip to content

Filtering, Ordering and Pagination

DRF provides comprehensive filtering, ordering, and pagination support for list endpoints. Enabling these features is as simple as configuring filter_backends and pagination_class on a view. This article also covers the exception-handling mechanism and how to handle business exceptions that DRF does not process by default.

Filtering

django-filter Integration

For exact-match or range-based field filtering, the recommended approach is the django-filter extension:

pip install django-filter

Register the app and configure the global filter backend:

# settings.py
INSTALLED_APPS = [
    ...
    'django_filters',
]

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
    ]
}

Specify filterable fields on a view:

from rest_framework.generics import ListAPIView
from django_filters.rest_framework import DjangoFilterBackend

class StudentListView(ListAPIView):
    queryset         = Student.objects.all()
    serializer_class = StudentSerializer
    filter_backends  = [DjangoFilterBackend]
    filterset_fields = ['age', 'sex', 'class_null']

# Client request: GET /students/?age=20&sex=true

Fine-Grained Filter Conditions

Use a custom FilterSet class to support range, membership, and fuzzy-match filtering:

import django_filters
from .models import Student

class StudentFilter(django_filters.FilterSet):
    age_min = django_filters.NumberFilter(field_name="age", lookup_expr="gte")
    age_max = django_filters.NumberFilter(field_name="age", lookup_expr="lte")
    name    = django_filters.CharFilter(field_name="name", lookup_expr="icontains")

    class Meta:
        model  = Student
        fields = ["age_min", "age_max", "name", "sex"]


class StudentListView(ListAPIView):
    queryset         = Student.objects.all()
    serializer_class = StudentSerializer
    filter_backends  = [DjangoFilterBackend]
    filterset_class  = StudentFilter

# Client request: GET /students/?age_min=18&age_max=30&name=zhang

Search Filter

DRF’s built-in SearchFilter supports fuzzy cross-field search:

from rest_framework.filters import SearchFilter

class StudentListView(ListAPIView):
    queryset         = Student.objects.all()
    serializer_class = StudentSerializer
    filter_backends  = [SearchFilter]
    search_fields    = ['name', 'description']

# Client request: GET /students/?search=zhang

search_fields prefix meanings:

PrefixBehaviourExample
(none)Fuzzy (icontains)'name'
^Starts with'^name'
=Exact match'=name'
@Full-text search (requires DB support)'@description'

Ordering

DRF’s built-in OrderingFilter lets clients control sort order via URL parameters:

from rest_framework.filters import OrderingFilter

class StudentListView(ListAPIView):
    queryset         = Student.objects.all()
    serializer_class = StudentSerializer
    filter_backends  = [OrderingFilter]
    ordering_fields  = ['id', 'age', 'name']   # Fields that may be sorted
    ordering         = ['-id']                  # Default ordering (optional)

# Ascending:  GET /students/?ordering=age
# Descending: GET /students/?ordering=-age
# Multi-field: GET /students/?ordering=class_null,-age

Combining Filters and Ordering

When using multiple filter backends simultaneously, list all of them (a per-view list overrides the global configuration):

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter

class StudentListView(ListAPIView):
    queryset         = Student.objects.all()
    serializer_class = StudentSerializer
    filter_backends  = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_fields = ['sex', 'class_null']
    search_fields    = ['name']
    ordering_fields  = ['id', 'age']
    ordering         = ['-id']

Pagination

PageNumberPagination (Page-Number Pagination)

The most commonly used pagination style; the client requests data by page number and page size:

from rest_framework.pagination import PageNumberPagination

class StandardPagination(PageNumberPagination):
    page_size             = 10        # Default items per page
    page_size_query_param = 'size'    # Parameter name for client-specified page size
    max_page_size         = 100       # Maximum items per page
    page_query_param      = 'page'    # Page-number parameter name (already default)


class StudentListView(ListAPIView):
    queryset         = Student.objects.all()
    serializer_class = StudentSerializer
    pagination_class = StandardPagination

# Example request: GET /students/?page=2&size=20

Paginated response format:

{
    "count": 100,
    "next": "http://127.0.0.1:8000/students/?page=3&size=20",
    "previous": "http://127.0.0.1:8000/students/?page=1&size=20",
    "results": [...]
}

LimitOffsetPagination (Offset Pagination)

Retrieve data by offset; ideal for infinite-scroll scenarios:

from rest_framework.pagination import LimitOffsetPagination

class StandardLimitOffsetPagination(LimitOffsetPagination):
    default_limit      = 10        # Default number of items to return
    max_limit          = 100       # Maximum number of items to return
    limit_query_param  = 'limit'   # Parameter name for item count
    offset_query_param = 'offset'  # Parameter name for starting offset


class StudentListView(ListAPIView):
    pagination_class = StandardLimitOffsetPagination

# Example request: GET /students/?limit=20&offset=40
# Meaning: starting at record 40, return 20 records

Global Pagination Configuration

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'myapp.pagination.StandardPagination',
    'PAGE_SIZE': 10,
}
Global pagination configuration affects every view that uses the list() method. For endpoints that do not need pagination (e.g., dropdown option lists), set pagination_class = None on the view to disable it.

Exception Handling

DRF Built-in Exceptions

DRF automatically handles the following exceptions and returns appropriate HTTP responses:

ExceptionTrigger ScenarioHTTP Status
ParseErrorRequest body parsing failed400
AuthenticationFailedAuthentication failure401
NotAuthenticatedUser not authenticated401
PermissionDeniedPermission denied403
NotFoundObject does not exist404
MethodNotAllowedHTTP method not permitted405
ThrottledRate limit exceeded429
ValidationErrorSerializer validation failed400

Custom Exception Handler

For exceptions DRF does not handle (e.g., database errors or business logic exceptions), register a custom handler:

# myapp/utils/exceptions.py
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status
from django.db import DatabaseError
import logging

logger = logging.getLogger(__name__)


def custom_exception_handler(exc, context):
    # Let DRF's default handler process the exception first
    response = exception_handler(exc, context)

    if response is not None:
        # DRF handled the exception; add a status_code field for consistency
        response.data['status_code'] = response.status_code
        return response

    # Handle exceptions that DRF did not process
    view = context.get('view')

    if isinstance(exc, DatabaseError):
        logger.error("[%s] Database error: %s", view.__class__.__name__, exc)
        return Response(
            {'detail': 'Database error, please try again later'},
            status=status.HTTP_503_SERVICE_UNAVAILABLE,
        )

    # Log and return 500 for all other unexpected exceptions
    logger.exception("[%s] Unexpected exception", view.__class__.__name__)
    return Response(
        {'detail': 'Internal server error'},
        status=status.HTTP_500_INTERNAL_SERVER_ERROR,
    )

Register the handler in settings:

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'myapp.utils.exceptions.custom_exception_handler',
}

Raising DRF Exceptions Explicitly in Views

from rest_framework.exceptions import NotFound, ValidationError, PermissionDenied

class StudentAPIView(APIView):
    def get(self, request, pk):
        try:
            student = Student.objects.get(pk=pk)
        except Student.DoesNotExist:
            raise NotFound(f"Student with id={pk} not found")
        return Response(StudentSerializer(student).data)

    def post(self, request):
        if not request.data.get("name"):
            raise ValidationError({"name": "Name cannot be empty"})
        ...

API Documentation

DRF supports automatic API documentation generation via coreapi or drf-spectacular.

Recommended: drf-spectacular (OpenAPI 3.0):

pip install drf-spectacular
# settings.py
INSTALLED_APPS = [..., 'drf_spectacular']

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

urlpatterns = [
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]

Visit http://127.0.0.1:8000/api/docs/ to view the Swagger UI documentation, which includes live testing support.

Last updated on