DRF: Pagination & Filtering

GET /tasks/ against TaskViewSet right now returns every single Task row in the database in one response. That's fine while TaskFlow has a handful of tasks — it stops being fine the moment a real project accumulates thousands of them: the response gets slow to generate, wasteful of bandwidth to transfer, and genuinely hard for a client app to do anything useful with all at once. This is exactly the same underlying concern Pagination already covered for ordinary HTML pages — an API response needs the same kind of "one page at a time" treatment.

PageNumberPagination: Applied Globally, Once

DRF's built-in PageNumberPagination class handles this, and rather than configuring it on every single ViewSet individually, it's usually set once, project-wide, in settings.py:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

With that in place, every ModelViewSet and generic view in the project — including TaskViewSet, with no changes to it at all — automatically paginates its list results at 10 per page, and automatically understands a ?page= query parameter for moving between pages.

What a Paginated Response Actually Looks Like

Turning pagination on changes the shape of a list response. Instead of a bare JSON array, DRF wraps the results in an object carrying a few extra, genuinely useful keys:

GET /api/tasks/?page=2

{
  "count": 47,
  "next": "http://localhost:8000/api/tasks/?page=3",
  "previous": "http://localhost:8000/api/tasks/?page=1",
  "results": [
    { "id": 11, "title": "Write updated copy", "priority": "medium", "completed": false },
    { "id": 12, "title": "Fix navbar spacing", "priority": "low", "completed": true }
  ]
}

count is the total number of matching tasks across every page, not just this one; next and previous are ready-to-use URLs for moving forward or backward (either one is null at the start or end of the list); results holds just this page's slice of tasks. A client never has to calculate page URLs itself — it just follows next until that field comes back null.

Query-String Filtering With django-filter

Pagination controls how much comes back at once; filtering controls which rows match in the first place. The standard package for this is django-filter, installed separately and wired into a ViewSet through filter_backends and filterset_fields:

pip install django-filter
# taskflow/views.py
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from .models import Task
from .serializers import TaskSerializer

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['priority', 'completed']

With filterset_fields in place, the client can now narrow the list purely through query parameters, no extra view code required:

GET /api/tasks/?priority=high&completed=false

That request returns only high-priority, not-yet-completed tasks. Under the hood, this is the exact same ORM filtering already covered in QuerySets & Filtering — django-filter essentially translates recognized query-string parameters into the equivalent Task.objects.filter(priority='high', completed=False) call automatically.

SearchFilter: Simple Text Search Across Fields

filterset_fields only matches exact values. For a looser, free-text search across one or more fields, DRF's built-in SearchFilter fits better:

# taskflow/views.py
from rest_framework import filters

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    filter_backends = [filters.SearchFilter]
    search_fields = ['title', 'description']
GET /api/tasks/?search=report

That request matches any task whose title or description contains "report" — a case-insensitive partial match across both fields at once, without writing any filtering logic in the view.

OrderingFilter: Letting the Client Choose the Sort Order

OrderingFilter hands the client control over sort order via a query parameter, rather than the API always returning results in one fixed order:

# taskflow/views.py
class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    filter_backends = [filters.OrderingFilter]
    ordering_fields = ['due_date', 'priority']
GET /api/tasks/?ordering=-due_date

The leading - reverses the sort, so this request returns tasks with the furthest-out due date first; dropping the - (?ordering=due_date) sorts soonest-first instead. All three filter backends above can be combined in the same filter_backends list, letting a single endpoint support filtering, searching, and ordering together.

Note: All of the pagination and filtering configuration above is correct, real DRF code — but this website's own project doesn't run a live Django server, so none of these requests can actually be made from this page.
Next up: The API itself is now solid — paginated, filterable, and properly secured. DRF: Consuming the API shows what it looks like to actually call it, from outside Django entirely.