Skip to content

DRF Querying and Pagination

DRF gives you simple tools to filter, search, sort, and paginate the data in your API. These tools help clients get exactly the data they want, in the amount they want, without slowing things down. Here’s a simple look at how all of this works:

graph TD Start["API Request"] -->|Query Parameters| Querying["DRF Querying (Filtering, Searching, Sorting)"] Querying -->|Filtered/Sorted Data| Pagination["DRF Pagination (Page Number, Limit-Offset)"] Pagination -->|Paginated Data| End["API Response"]
Reference Code for Models and Serializers
# models.py
from django.db import models
class Collection(models.Model):
title = models.CharField(max_length=255)
class Product(models.Model):
title = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
collection = models.ForeignKey('Collection', on_delete=models.CASCADE)
# serializers.py
from rest_framework import serializers
from .models import Product, Collection
class CollectionSerializer(serializers.ModelSerializer):
class Meta:
model = Collection
fields = ['id', 'title']
class ProductSerializer(serializers.ModelSerializer):
collection = CollectionSerializer()
class Meta:
model = Product
fields = ['id', 'title', 'price', 'collection']

Filtering means letting clients pick what data they want to see, based on some rule. For example, “only show me products from collection number 1.” DRF makes this easy with query parameters (the part after ? in a URL). You can also use a package called django-filter to add filtering with much less code.

There are two main ways to add filtering:

  • Query Parameter Filtering: The client adds a filter directly in the URL.

  • django-filter: A package that gives you an easy and flexible way to filter data based on model fields.

You can add simple filtering by changing the get_queryset method in your view. For example:

# views.py
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_queryset(self):
queryset = super().get_queryset()
collection_id = self.request.query_params.get('collection_id', None)
if collection_id is not None:
queryset = queryset.filter(collection_id=collection_id)
return queryset

This lets clients filter products by collection ID using a URL like ?collection_id=1. With this in place, clients can get only the data they actually need, instead of pulling everything.

Django-filter is a handy package that makes adding filters to your DRF views much simpler. It lets you set up filters based on your model fields and gives clients a clean way to use them. Here’s how to set it up:

  1. First, install django-filter:

    Terminal window
    uv add django-filter
  2. Then add it to your INSTALLED_APPS in settings.py:

    INSTALLED_APPS = [
    # other apps
    'django_filters',
    ]
# views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['collection_id']

If you need more advanced filtering, you can build your own filter class using django-filter. This lets you choose different filter types for each field. For example:

# filters.py
from django_filters.rest_framework import FilterSet
from .models import Product
class ProductFilter(FilterSet):
class Meta:
model = Product
fields = {
'price': ['gt', 'lt'],
'collection_id': ['exact'],
}

This custom filter lets clients filter products by price using “greater than” or “less than,” and still filter by collection_id using an exact match. Here’s a simple table showing what each lookup type means:

LookupMeaning
exact=
iexactcase-insensitive exact
containsLIKE %x%
icontainscase-insensitive contains
inIN (...)
gt / gte> / >=
lt / lte< / <=
rangeBETWEEN
isnullIS NULL
startswithLIKE x%
istartswithcase-insensitive startswith
endswithLIKE %x
iendswithcase-insensitive endswith

Now you can use this custom filter in your view:

# views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
from .filters import ProductFilter
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = ProductFilter

You can build basic search using query parameters, but it gets tricky if you want to search across many fields at once, or want the search to ignore uppercase and lowercase letters. DRF solves this with a built-in tool called SearchFilter. To use it, add it to your view’s filter_backends, then list the fields you want to search using search_fields. For example:

# views.py
from rest_framework import viewsets
from rest_framework.filters import SearchFilter
from .models import Product
from .serializers import ProductSerializer
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [SearchFilter]
search_fields = [
'title',
'description',
'collection__title', # you can also search on related
# fields using double underscores
]

With search set up, clients can easily find what they’re looking for across several fields at once. The URL for searching looks like this: ?search=keyword, where keyword is whatever you’re searching for. This makes your API much easier to use.

Sorting lets clients choose the order in which they get results back. DRF has a built-in tool for this called OrderingFilter. To use it, add it to your view’s filter_backends, then list which fields can be sorted using ordering_fields. For example:

# views.py
from rest_framework import viewsets
from rest_framework.filters import OrderingFilter
from .models import Product
from .serializers import ProductSerializer
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [OrderingFilter]
ordering_fields = ['title', 'price', 'last_update']

With sorting set up, clients can choose how they want their results arranged, based on whichever field works best for them. The URL for sorting looks like this: ?ordering=field_name, where field_name is the field you want to sort by. To sort in reverse (descending) order, just add a hyphen before the field name, like this: ?ordering=-field_name. This gives clients full control over how they view your data.

Pagination means breaking up a large set of results into smaller chunks, instead of sending everything at once. This makes your API faster and saves bandwidth. DRF comes with built-in pagination classes that you can use right away.

There are two ways to set up pagination:

  • Global Pagination: Set a default pagination class in settings.py. This applies to every view that supports pagination.

  • View-Level Pagination: Set a pagination class on a specific view using the pagination_class attribute.

There are two common types of pagination:

  • Page Number Pagination: The client asks for a specific page number and how many items they want per page.

  • Limit-Offset Pagination: The client says how many items they want (limit) and where to start from (offset).

To set a default pagination class for your whole API, add this to your settings.py file:

# settings.py
REST_FRAMEWORK = {
# other settings
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
# you can also use 'rest_framework.pagination.LimitOffsetPagination' for limit-offset pagination
'PAGE_SIZE': 10,
}

To set pagination for just one view, set the pagination_class attribute on that view. For example:

# pagination.py
from rest_framework.pagination import PageNumberPagination
from rest_framework.pagination import LimitOffsetPagination
class ProductPagination(PageNumberPagination):
page_size = 10
class ProductLimitOffsetPagination(LimitOffsetPagination):
default_limit = 10
# views.py
from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer
from .pagination import ProductPagination, ProductLimitOffsetPagination
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
pagination_class = ProductPagination # or ProductLimitOffsetPagination

With pagination set up, your API runs faster and gives clients data in smaller, easy-to-handle pieces. The URL for page number pagination looks like this: ?page=2, where 2 is the page you want. For limit-offset pagination, the URL looks like this: ?limit=10&offset=20, where limit is how many items you want and offset is where to start counting from. This way, clients can move through large amounts of data without overloading their apps or using too much bandwidth.