Django REST Filtering Tutorial - Intro (Part I)
Tags: django, djangorestframework, filter, search
Published: 2022-04-09 12:06:53

---
title: Django REST Filtering Tutorial - Intro (Part I)
published: true
date: 2022-03-17 00:00:00 UTC
tags: django, djangorestframework, filter, search
canonical_url: https://tech.serhatteker.com/post/2022-03/django-rest-filtering-tutorial-part-1/
---

## Entrée

In this tutorial I'd like to aim to collect some of **Django REST** **filtering** concepts and common examples that I use and teach usually. Some of them can be found in official Django REST documentation, however I'm going to walk by a sample repository.

The repo consists as less code as possible which implies it's not designed according to best practices such as *api versioning* (`api/v1`), [The Twelve Factor App] etc.

> NOTE
>
> You can find the code we're working on this tutorial series in this repository: [django-rest-filtering-tutorial](https://github.com/SerhatTeker/django-rest-filtering-tutorial)
>
> You don't need to download and setup the repository in order to follow this series, however I'd like to encorauge to do so, since while you play you learn better as always.


## Setup Repo

As mentioned above you don't need to setup the repo however if you like to, —I suggest as well, run below commands:

```bash
# Create virtual environment named .venv
$ python -m venv ./.venv
# Activate the virtual environment
$ source ./.venv/bin/activate
# Install python packages
$ python -m pip install -r requirements.txt
# Create db, make migrations and seed database with some dummy data
$ python populate_db.py
# Run development server
$ python manage.py runserver 8000
# or
# ./manage.py runserver 8000
```

> INFO
>
> You don't need to give `8000` port since it's **8default port** but I always prefer **verbosity**.

Also another option is that you can use [gnu make] and simply run:

```bash
$ make start
```

This **make** target will run all commands above together so it will setup the repository and run development server.


## Sending HTTP Requests

I'm going to use [httpie] for **HTTP** *requests* since it's more human-usable —or let's say more dev-friendly; it provides colorful syntax highlighting (like pretty JSON outputs), persistent sessions, capability of downloading files etc. and it's written in **Python** but you can use [curl] as well:

```bash
$ http GET http://localhost:8000/authors/
# or with less keystroke, it's same as above
$ http :8000/authors/
# curl equivalent
$ curl http://localhost:8000/authors/
```

For more info you can look at [httpie doc].

![httpie-demo](https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif)

Let's start with some basics and send http requests to endpoints.

In order to get all *authors*, run:

```bash
$ http :8000/authors/

HTTP/1.1 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 165
Content-Type: application/json
Date: Fri, 17 Mar 2022 11:03:37 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.11
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

[
    {
        "first_name": "Name1",
        "full_name": "Name1 Surname1",
        "id": 1,
        "last_name": "Surname1",
        "user": null
    },
    {
        "first_name": "Name2",
        "full_name": "Name2 Surname2",
        "id": 2,
        "last_name": "Surname2",
        "user": null
    },
    {
        "first_name": "Name2",
        "full_name": "Name2 Surname2",
        "id": 3,
        "last_name": "Surname2",
        "user": 3
    }
]
```

Mind the slash (`/`) suffix after *authors*; if you don't append it you will get:

```bash
$ http :8000/authors

HTTP/1.1 301 Moved Permanently
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Fri, 17 Mar 2022 11:05:35 GMT
Location: /authors/
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.11
X-Content-Type-Options: nosniff
```

You can change this behavior by adding `--follow` argument as a flag or as a config in [httpie]'s config file which by default located in `~/.config/httpie/config.json`.  As a command flag:

```bash
$ http :8000/authors --follow
```

As a config:

```json
// ~/.config/httpie/config.json
{
    "__meta__": {
        "about": "HTTPie configuration file",
        "help": "https://httpie.org/doc#config",
        "httpie": "1.0.3"
    },
    "default_options": [
        "--timeout=300",
        "follow"
    ]
}
```

Django controls this behavior by **[APPEND_SLASH]** setting:

> **APPEND_SLASH**
>
> Default: **True**
>
> When set to **True**, if the request URL does not match any of the patterns in the URLconf and it doesn’t end in a slash, an HTTP redirect is issued to the same URL with a slash appended. Note that the redirect may cause any data submitted in a POST request to be lost.
>
> The **APPEND_SLASH** setting is only used if **[CommonMiddleware]** is installed (see [Middleware]). See also **[PREPEND_WWW]**.

Also if you'd like to omit `header` part of the response provide `--body` flag.


## Default Filtering

Default filter for [Django REST] is **no filter**, meaning that it returns all *objects* from the database.

In other words if you try to filter the endpoint with some parameters it will return all objects together just like it did above:

```bash 
$ http ":8000/authors/?id=1"

[
    {
        "first_name": "Name1",
        "full_name": "Name1 Surname1",
        "id": 1,
        "last_name": "Surname1",
        "user": null
    },
    {
        "first_name": "Name2",
        "full_name": "Name2 Surname2",
        "id": 2,
        "last_name": "Surname2",
        "user": null
    },
    {
        "first_name": "Name2",
        "full_name": "Name2 Surname2",
        "id": 3,
        "last_name": "Surname2",
        "user": 3
    }
]
```

For better understanding let's look at the responsible [get_queryset] method from the source code:

```python
# rest_framework/generics.py
def get_queryset(self):
    """
    Get the list of items for this view.
    This must be an iterable, and may be a queryset.
    Defaults to using `self.queryset`.

    This method should always be used rather than accessing `self.queryset`
    directly, as `self.queryset` gets evaluated only once, and those results
    are cached for all subsequent requests.

    You may want to override this if you need to provide different
    querysets depending on the incoming request.

    (Eg. return a list of items that is specific to the user)
    """
    assert self.queryset is not None, (
        "'%s' should either include a `queryset` attribute, "
        "or override the `get_queryset()` method."
        % self.__class__.__name__
    )

    queryset = self.queryset
    if isinstance(queryset, QuerySet):
        # Ensure queryset is re-evaluated on each request.
        queryset = queryset.all()
    return queryset
```

As you can see, it just returns `queryset` attribute from the related view which in our case is `Article.objects.all()`:

```python
# src/articles/view.py
from rest_framework import viewsets

from src.articles.models import Article
from src.articles.serializers import ArticleSerializer


class ArticleViewSet(viewsets.ModelViewSet):
    serializer_class = ArticleSerializer
    queryset = Article.objects.all()
```

So as an example if you change `queryset` attribute to `Article.objects.filter(content__icontains="3")`,

```python
# src/articles/view.py
from rest_framework import viewsets

from src.articles.models import Article
from src.articles.serializers import ArticleSerializer


class ArticleViewSet(viewsets.ModelViewSet):
    serializer_class = ArticleSerializer
    queryset = Article.objects.filter(content__icontains="3")
```

it only will return **Fake Article3** article:

```bash
$ http ":8000/authors/"
# or
$ http ":8000/authors/?id=1"

[
    {
        "author": 3,
        "content": "Fake Content3",
        "id": 3,
        "regions": [],
        "title": "Fake Article3"
    }
]
```

At the same time you may realize you can directly change the `get_queryset` method and throw the `queryset` attribute:

```python
# src/articles/views.py
class ArticleViewSet(viewsets.ModelViewSet):
    serializer_class = ArticleSerializer

    def get_queryset(self):
        return Article.objects.filter(content__icontains="3")
```

## Conclusion

That's all for this part of the series. In the next parts you will explore more in depth example and cases.

All done!




[Django REST]: https://www.django-rest-framework.org/
[django-rest-filtering-tutorial]: https://github.com/SerhatTeker/django-rest-filtering-tutorial
[httpie]: https://httpie.io/
[httpie doc]: https://httpie.io/docs/cli
[httpie-demo]: https://raw.githubusercontent.com/httpie/httpie/master/docs/httpie-animation.gif
[curl]: https://curl.se/
[The Twelve Factor App]: https://12factor.net/
[APPEND_SLASH]: https://docs.djangoproject.com/en/4.0/ref/settings/#append-slash
[Middleware]: https://docs.djangoproject.com/en/4.0/topics/http/middleware/
[CommonMiddleware]: https://docs.djangoproject.com/en/4.0/ref/middleware/#django.middleware.common.CommonMiddleware
[PREPEND_WWW]: https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-PREPEND_WWW
[get_queryset]: https://github.com/encode/django-rest-framework/blob/0323d6f8955f987771269506ca5da461e2e7a248/rest_framework/generics.py#L48-L73
[gnu make]: https://www.gnu.org/software/make/