RizTech Academy logo
RizTech Academy
Building an API with DRFLesson 3 of 530 min

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.

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 the django-filter package).

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

  1. Add SearchFilter and OrderingFilter with search_fields and ordering_fields; reproduce ?search=Asha → 1 and ?ordering=-name → "Chetan" first.
  2. Try ?ordering= a field not in ordering_fields and confirm it is ignored; reason about why the whitelist protects you.
  3. Install django-filter, add DjangoFilterBackend and filterset_fields = ["city"]; confirm ?city=Pune filters.
  4. Write an AppointmentFilter with a min_fee range filter and a status choice; test ?status=booked&min_fee=500.
  5. Combine ?city=Pune&search=asha&ordering=-name&page=1 and confirm filtering/ordering happen before pagination.
  6. Hand-roll a get_queryset filter for city, then replace it with filterset_fields; note what the declarative version adds (browsable-API UI, validation).

Official documentation

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