Filtering, searching, and ordering results
Pagination controls how much of the list comes back at once; filtering controls which rows are in that list to begin with. This part covers the three DRF backends that cover almost every filtering need: exact-match filtering, free-text search, and client-controlled ordering.
Exact-match filtering with django-filter
pip install django-filter# taskapi/settings.py
INSTALLED_APPS = [
# ...
"django_filters",
]
REST_FRAMEWORK = {
# ... existing settings ...
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
}# tasks/views.py
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
filterset_fields = ["completed", "owner"]filterset_fields generates exact-match filtering for each listed field automatically — no filter logic written by hand. /api/tasks/?completed=true now returns only completed tasks; /api/tasks/?completed=true&owner=3 combines both as an AND, the same way chained .filter() calls in plain Django do.
Free-text search
from rest_framework.filters import SearchFilter
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
filterset_fields = ["completed", "owner"]
filter_backends = [DjangoFilterBackend, SearchFilter]
search_fields = ["title", "description"]/api/tasks/?search=api now matches any task whose title or description contains "api" (case-insensitive) — SearchFilter runs an icontains lookup across every field in search_fields and ORs the results together, unlike filterset_fields' exact-match ANDing. Note filter_backends is now set explicitly on the class, listing both backends — once any view declares its own filter_backends, it overrides the global DEFAULT_FILTER_BACKENDS entirely rather than adding to it.
Client-controlled ordering
from rest_framework.filters import OrderingFilter
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
filterset_fields = ["completed", "owner"]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
search_fields = ["title", "description"]
ordering_fields = ["due_date", "created_at"]/api/tasks/?ordering=due_date
/api/tasks/?ordering=-due_dateordering_fields is an explicit allowlist — a client can only sort by fields listed here, never by an arbitrary column name passed in the query string. That's a deliberate restriction, not a limitation: allowing ?ordering= to pass through to the database unchecked would let a client sort by any column on the table, including ones that were never meant to be client-facing at all.
Combining all three
/api/tasks/?completed=false&search=api&ordering=due_dateAll three backends run together on one request — filter to incomplete tasks, search within that filtered set for "api", then sort the result by due date. Each backend narrows or reorders whatever the previous one produced, the same chaining logic as stacked .filter().filter().order_by() calls in plain Django.
Leaving ordering_fields unset while OrderingFilter is in filter_backends. Without an explicit allowlist, DRF permits ordering by any model field by default — including ones that were deliberately left out of the serializer, which can leak information about a field's existence (or let a client sort by something like an internal-only score field) that was never meant to be exposed.
Next: authentication — every endpoint so far has worked without logging in at all, since nothing has required it yet.