
Filtering in DRF
If you haven't using the django-filter
package with the Django REST Framework (DRF), then it's time you give it a whirl!
When I saw how django-filter
relates to DRF, I was thrilled. I feel like this method I'm about to show you is somewhat of a life-hack. I realize that not every situation is going to be the right situation for this, but in general I think you get a maintainable codebase by using django-filter
instead of manually checking and testing request parameters.
Here is a quick example. First off, create a new directory and django project:
$ mkdir filtering
$ cd filtering
$ python3 -m venv env
$ . env/bin/activate
$ pip install "Django~=5.2" djangorestframework django-filter
Then we'll create a project and add a simple app:
$ django-admin startproject filtering .
$ ./manage.py startapp api
Next, tell Django that you're going to use DRF and filters. In the settings.py
file, under INSTALLED_APPS
, add these:
...
'rest_framework',
'django_filters',
'api',
...
Make sure everything runs at this point:
$ ./manage.py runserver
Default Filtering
I want to apply filtering to all my viewsets, instead of one at a time. To do that, configure this setting in the settings.py
file:
REST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
}
Model & ViewSet
Lets add a model that we can filter on. In the api/models.py
file:
from django.db import models
from django.utils.translation import gettext_lazy as _
class Taco(models.Model):
name = models.CharField(max_length=15)
TORTILLA_CORN = "corn"
TORTILLA_FLOUR = "flour"
TORTILLA_CHOICES = (
(TORTILLA_CORN, _("Corn Tortilla")),
(TORTILLA_FLOUR, _("Flour Tortilla")),
)
tortilla = models.CharField(
max_length=5,
choices=TORTILLA_CHOICES,
default=TORTILLA_FLOUR,
)
Then make the migration for that, and migrate:
$ ./manage.py makemigrations
$ ./manage.py migrate
Then let's add some data:
$ ./manage.py shell
>>> from api.models import Taco
>>> Taco.objects.create(name="Cheese", tortilla=Taco.TORTILLA_FLOUR)
>>> Taco.objects.create(name="Frijole", tortilla=Taco.TORTILLA_CORN)
In the api/serializers.py
file:
from rest_framework import serializers
from .models import Taco
class TacoSerializer(serializers.ModelSerializer):
class Meta:
model = Taco
read_only_fields = ()
exclude = ()
Then in the api/views.py
file:
from .serializers import TacoSerializer
from .models import Taco
from rest_framework import viewsets
class TacoViewSet(viewsets.ModelViewSet):
queryset = Taco.objects.all()
serializer_class = TacoSerializer
Lastly, we need a URL route. In the filtering/urls.py
file:
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from api.views import TacoViewSet
router = DefaultRouter()
router.register(r'tacos', TacoViewSet)
urlpatterns = [
path('api/', include(router.urls)),
path('admin/', admin.site.urls),
]
Now run the project, and check the tacos!
./manage.py runserver
Link -> http://localhost:8000/api/tacos/
But.... there is no filtering on this yet. The next steps are easy. First, lets create a filter for the tacos, using the api/filters.py
file:
import django_filters
from .models import Taco
class TacoFilter(django_filters.FilterSet):
class Meta:
model = Taco
fields = {
"name": ["icontains"],
"tortilla": ["exact"],
}
Then we just simply add this filter into the viewset, api/views.py
:
...
class TacoViewSet(viewsets.ModelViewSet):
queryset = Taco.objects.all()
serializer_class = TacoSerializer
filterset_class = TacoFilter
Now if you run the dev server, and visit the API endpoint, you should notice a button that says "Filter," allowing you to easily filter the results.
Conclusion
I like this method for adding filtering to API endpoints, because it provides a simple, repeatable, programmatic way, to add filtering to an endpoint. You don't need to extract the query parameters and add a special and unique test for each one. Instead, you can tie the model directly to the filter.