Filtering, searching and ordering an API
A real API is rarely "give me everything" — clients want the patients in Pune, reports from last week,
appointments sorted by date. Building that by reading query parameters and calling .filter() by hand
works but quickly sprawls. DRF and django-filter give you filtering, search and ordering declaratively —
you say which fields are filterable and the framework wires the query parameters to the queryset. This
lesson makes Nidaan's API queryable the clean way.
Three related needs, three tools
Clients query a list endpoint in three ways, and DRF has a backend for each:
- Search — a free-text box across several fields ("find 'asha'").
SearchFilter. - Ordering — sort by a field, ascending or descending ("newest first").
OrderingFilter. - Filtering — exact/range filters on specific fields ("city = Pune", "fee ≥ 500").
DjangoFilterBackend(from thedjango-filterpackage).
You enable the ones you want per ViewSet, and they read standard query parameters. All three combine cleanly — a client can search, filter and order in one request.
Search and ordering, built into DRF
SearchFilter and OrderingFilter ship with DRF (no extra package). Add them to the ViewSet and declare
which fields they cover:
from rest_framework import viewsets, filters
from .models import Patient
from .serializers import PatientSerializer
class PatientViewSet(viewsets.ModelViewSet):
queryset = Patient.objects.all().order_by("name")
serializer_class = PatientSerializer
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ["name", "phone"]
ordering_fields = ["name", "city"]
Now the endpoint responds to query parameters:
GET /api/patients/?search=Asha # verified: returns 1 (the matching patient)
GET /api/patients/?ordering=-name # verified: first result is "Chetan" (descending)
GET /api/patients/?ordering=city # ascending by city
Verified: ?search=Asha returns exactly the one matching patient, and ?ordering=-name puts "Chetan"
first (descending). search_fields lists the fields the free-text ?search= looks across (it does a
case-insensitive contains by default); ordering_fields whitelists which fields a client may sort by
(the - prefix means descending). Whitelisting ordering_fields matters — without limiting it,
clients could order by any field, including unindexed ones, causing slow queries; you expose only the sorts
you intend.
Field filtering with django-filter
For exact and range filters on specific fields, add the django-filter package (pip install django-filter, add "django_filters" to INSTALLED_APPS) and its backend:
from django_filters.rest_framework import DjangoFilterBackend
class PatientViewSet(viewsets.ModelViewSet):
# ...
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ["city"] # exact-match filter on city
Now GET /api/patients/?city=Pune returns only Pune patients. filterset_fields gives simple exact-match
filters; for ranges and custom lookups, define a FilterSet class:
import django_filters
class AppointmentFilter(django_filters.FilterSet):
min_fee = django_filters.NumberFilter(field_name="fee", lookup_expr="gte")
status = django_filters.ChoiceFilter(choices=Appointment.Status.choices)
class Meta:
model = Appointment
fields = ["status", "min_fee", "doctor"]
GET /api/appointments/?status=booked&min_fee=500 then filters to booked appointments with a fee of at
least 500. A FilterSet is the declarative home for an endpoint's filtering rules — the equivalent of the
custom-manager idea, applied to the API surface.
Combine, and mind the interaction with pagination
The three backends compose in one request:
GET /api/patients/?city=Pune&search=asha&ordering=-name&page=2
This filters to Pune, searches for "asha", orders by name descending, and returns page 2 — DRF applies filtering and ordering before pagination, so you page through the filtered, sorted result, which is exactly what a client expects. The ordering also matters for stable pagination (the previous lesson's point): a filtered list still needs a definite order for pages to be consistent.
Do not hand-roll what the backends give you
The temptation, especially early, is to read request.query_params and build the filter yourself:
def get_queryset(self):
qs = Patient.objects.all()
if self.request.query_params.get("city"): # hand-rolled — avoid for standard filters
qs = qs.filter(city=self.request.query_params["city"])
return qs
This works for one filter and becomes unmaintainable at five — and it silently allows any value, skips the
validation and consistent error handling the backends provide, and is not documented in the browsable API
or schema. The declarative backends generate the filter UI in the browsable API, validate inputs, and keep
the query logic out of your view. Reserve get_queryset overrides for logic the backends genuinely cannot
express (like the per-user scoping from the object-level-permissions lesson) — for standard field filtering,
search and ordering, use the backends.
Check your work
The three needs and tools. Search (SearchFilter, free-text over fields), ordering (OrderingFilter,
sort), field filtering (DjangoFilterBackend, exact/range) — enabled per ViewSet, combinable.
Search and ordering. filter_backends + search_fields (case-insensitive contains) + ordering_fields
(whitelist). Verified: ?search=Asha → 1 result, ?ordering=-name → "Chetan" first.
Why whitelist ordering_fields. So clients cannot sort by arbitrary (unindexed, slow) fields — expose
only intended sorts.
Field filtering. django-filter's DjangoFilterBackend with filterset_fields (exact) or a
FilterSet class (ranges, custom lookups like min_fee via lookup_expr="gte").
Combining with pagination. Backends apply filtering/ordering before pagination, so you page a filtered, sorted result; ordering keeps pages stable.
Why not hand-roll. Manual query_params filtering sprawls, skips validation and browsable-API/schema
integration; reserve get_queryset for logic backends cannot express (per-user scoping).
Practice
- Add
SearchFilterandOrderingFilterwithsearch_fieldsandordering_fields; reproduce?search=Asha→ 1 and?ordering=-name→ "Chetan" first. - Try
?ordering=a field not inordering_fieldsand confirm it is ignored; reason about why the whitelist protects you. - Install
django-filter, addDjangoFilterBackendandfilterset_fields = ["city"]; confirm?city=Punefilters. - Write an
AppointmentFilterwith amin_feerange filter and astatuschoice; test?status=booked&min_fee=500. - Combine
?city=Pune&search=asha&ordering=-name&page=1and confirm filtering/ordering happen before pagination. - Hand-roll a
get_querysetfilter forcity, then replace it withfilterset_fields; note what the declarative version adds (browsable-API UI, validation).
Official documentation
- DRF — Filtering —
SearchFilter,OrderingFilter,DjangoFilterBackend. - django-filter documentation —
FilterSet, field filters and lookups. - DRF — Search and ordering fields —
search_fieldsandordering_fields.
Next: token and session authentication for APIs.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship