skip to content

Filtering, Search & Ordering

filter_backends apply DjangoFilterBackend from django-filter, SearchFilter and OrderingFilter to a view's queryset. Interviewers ask why ordering_fields must be explicit and when get_queryset filters.

part ofDjango REST Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Django REST Framework, how do filter_backends let clients filter, search and order a list endpoint such as GET /jobs/?

level: juniorimportance: must knowfreq 62%

answer

  1. a list of classes on the view
  2. each narrows the previous queryset
  3. django-filter supplies the field filters
  4. ?search= and ?ordering= parameters
  5. empty default, view list replaces it

basics

~10 s

A DRF generic view passes its queryset through each class in filter_backends: DjangoFilterBackend (django-filter) handles field filters like ?remote=true, SearchFilter handles ?search= over search_fields, and OrderingFilter handles ?ordering= limited to ordering_fields.

solid answer

~40 s

DRF generic views pass their queryset through every class listed in `filter_backends`, in order, and each backend returns a narrower or reordered `QuerySet`. `DjangoFilterBackend` comes from the separate django-filter package and turns `filterset_fields` (or a `filterset_class`) into per-field parameters such as `?remote=true`. `SearchFilter` reads `?search=` and matches the terms against `search_fields`; `OrderingFilter` reads `?ordering=` and applies `order_by()` limited to `ordering_fields`, falling back to the view's `ordering`. `DEFAULT_FILTER_BACKENDS` defaults to an empty list, and a view-level `filter_backends` replaces that default rather than adding to it. The backends run in `list()` and also inside `get_object()`, so a filter parameter can turn a detail request into a 404.

code

python · 17 lines
python
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, generics

from jobs.models import Job
from jobs.serializers import JobSerializer


class JobListView(generics.ListAPIView):
    queryset = Job.objects.select_related("company")
    serializer_class = JobSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ["remote", "seniority", "company"]
    search_fields = ["title", "description", "company__name"]
    ordering_fields = ["posted_at", "salary_max"]
    ordering = ["-posted_at"]

# GET /jobs/?remote=true&search=python&ordering=-salary_max

go deeper

for a junior

Name the three backends, the query parameter each reads and the view attribute each needs. Know that django-filter is a separate install that goes into INSTALLED_APPS.

for a middle

Explain that filter_queryset chains backends in list order, that the global default is empty, and that a view's filter_backends replaces the default instead of extending it.

for a senior

Point out that backends also run inside get_object, so filter parameters can 404 detail and write requests, and decide which rules belong in get_queryset versus a reusable backend.

for a principal

Frame filter backends as the API's public query surface: which parameters are documented and supported long term, and how a shared backend keeps dozens of list endpoints consistent.

## What a filter backend is In Django REST Framework (DRF), a **filter backend** is a small class that takes the view's queryset and returns a narrower or reordered one. Every backend subclasses `BaseFilterBackend` and implements one method, `filter_queryset(self, request, queryset, view)`. The backend reads the request (usually `request.query_params`) and attributes declared on the view, and returns a new `QuerySet`. Because Django QuerySets are lazy, each backend only adds clauses to the SQL that will eventually run; nothing reaches the database until the view paginates or serializes the result. The backends a view uses are listed in its `filter_backends` attribute. `GenericAPIView.filter_queryset()` loops over that list **in order** and feeds each backend the output of the previous one, so the backends compose like a pipeline. ## The three backends you will meet | Backend | Comes from | Query parameter | View attribute it reads | What it adds | |---|---|---|---|---| | `DjangoFilterBackend` | the `django-filter` package (`django_filters.rest_framework`) | one per field, e.g. `?remote=true` | `filterset_fields` or `filterset_class` | field-by-field filters | | `SearchFilter` | `rest_framework.filters` | `?search=` (setting `SEARCH_PARAM`) | `search_fields` | a free-text match across several fields | | `OrderingFilter` | `rest_framework.filters` | `?ordering=` (setting `ORDERING_PARAM`) | `ordering_fields`, `ordering` | an `order_by()` chosen by the client | For a job-listings API, `GET /jobs/?remote=true&search=python&ordering=-salary_max` keeps remote jobs, then keeps those whose title, description or company name contains 'python', then sorts by maximum salary, highest first. - `filterset_fields` gives simple **equality** filters: django-filter builds a `FilterSet` for the listed fields automatically. For ranges, choice lists or custom lookups you write a `FilterSet` and point `filterset_class` at it. - `SearchFilter` does nothing unless the view sets `search_fields` **and** the request carries a non-empty search term. - `OrderingFilter` acts even without `ordering_fields` (it then allows the serializer's readable fields) and falls back to the view's `ordering` attribute when the client sends no valid ordering. ## Configuring backends: global default vs per view The project-wide default lives in the `REST_FRAMEWORK` settings dict under `DEFAULT_FILTER_BACKENDS`. Its default value is an **empty list**, so a fresh DRF project filters nothing: parameters such as `?remote=true` are simply ignored until a backend is configured. To use django-filter you: 1. install the `django-filter` package; 2. add `'django_filters'` to `INSTALLED_APPS`; 3. list `django_filters.rest_framework.DjangoFilterBackend` in `DEFAULT_FILTER_BACKENDS` or in a view's `filter_backends`. The per-view `filter_backends` is a plain class attribute whose default **is** the setting. Assigning it on a view therefore **replaces** the global list. A project that sets `DjangoFilterBackend` globally and then writes `filter_backends = [filters.SearchFilter]` on one view has silently switched off field filtering on that view; repeat every backend you still want. ## Where the backends run `filter_queryset()` is called from two places in DRF's generic machinery: - `ListModelMixin.list()` runs `self.filter_queryset(self.get_queryset())` before pagination and serialization; - `GenericAPIView.get_object()` runs the same expression before looking the object up by `lookup_field`. The second call surprises people: filters also apply to retrieve, update, partial update and destroy. `GET /jobs/42/?remote=false` returns 404 for a remote job, because the filtered queryset no longer contains it. `create()` never touches the queryset, so filter parameters have no effect on `POST /jobs/`. ## Common mistakes - Expecting filtering with no configuration, when `DEFAULT_FILTER_BACKENDS` is empty. - Importing `DjangoFilterBackend` from `rest_framework.filters`: DRF removed its own copy in 3.7 and the class lives in django-filter. - Forgetting that a view-level `filter_backends` drops the global backends. - Treating a client-controlled filter parameter as a security boundary; what a client may omit restricts nothing. ## Writing your own backend When the same narrowing is needed on many views, subclass `BaseFilterBackend` and override `filter_queryset()`. Implement `get_schema_operation_parameters()` if the parameter should appear in the generated OpenAPI schema, and `to_html()` if it should render a control in the browsable API. The method must return a `QuerySet` so the next backend and the paginator can keep chaining.

  • How would you add a filter that every job view needs, such as hiding draft listings, without repeating get_queryset() everywhere?
    Subclass `BaseFilterBackend`, override `filter_queryset(self, request, queryset, view)` to return `queryset.filter(status="published")`, and add the class to `DEFAULT_FILTER_BACKENDS` or to each view's `filter_backends`. It runs in `list()` and `get_object()` like any backend. Remember that a view which sets its own `filter_backends` must include it again, or the rule disappears there.
  • Do filter backends affect POST /jobs/ on a ModelViewSet?
    No. `CreateModelMixin.create()` validates the body and saves through the serializer without calling `get_queryset()` or `filter_queryset()`. Backends run only where the queryset is read: `list()`, and every action that goes through `get_object()`, namely retrieve, update, partial update and destroy.

Filter backends are stations on a sorting line: each station takes the tray the previous one passed on and sifts it further. Installing your own line on one view replaces the whole factory line there, it does not bolt stations onto it.

saying these in an interview costs you the question

  • DRF filters list endpoints out of the box with no configuration
  • Setting filter_backends on a view adds to DEFAULT_FILTER_BACKENDS
  • DjangoFilterBackend is imported from rest_framework.filters
  • Filter backends only run on list requests, never on detail lookups
  • SearchFilter searches every model field when search_fields is omitted
open as a page

In Django REST Framework, why should a view using OrderingFilter declare ordering_fields explicitly, and what happens if you leave it unset or use '__all__'?

level: middleimportance: must knowfreq 50%

basics

~10 s

OrderingFilter's ordering_fields is the whitelist for ?ordering=. Unset, it allows every readable serializer field; 'all' allows every concrete model field and annotation. An explicit list stops clients sorting by hidden or unindexed columns.

open as a page

In Django REST Framework generic views, when should filtering live in get_queryset() rather than in a filter backend, and how do the two combine?

level: middleimportance: should knowfreq 47%

basics

~10 s

Put rules that always apply, or come from the URL, in get_queryset(); put optional, client-driven narrowing in filter backends. DRF runs filter_queryset(get_queryset()), so backends narrow the base set on list and every get_object() action.

open as a page

In Django REST Framework's SearchFilter, what do the ^, =, @ and $ prefixes on search_fields do, and how are several search terms combined?

level: middleimportance: should knowfreq 42%

basics

~10 s

SearchFilter prefixes choose the lookup: ^ istartswith, = iexact, @ full-text search (PostgreSQL only), $ iregex, none icontains. Each search term must match at least one of the search_fields, and all terms must match.

open as a page

A DRF job-listings endpoint over millions of rows passes ?sort= straight to order_by() and uses SearchFilter with '$description'; what breaks in production and how do you harden it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Raw order_by() lets clients sort by any related or hidden field, by '?' for random order, or crash with FieldError (a 500); $ lets them run costly regexes. Use OrderingFilter with indexed ordering_fields, a unique tiebreaker, and non-regex search.

open as a page