API for resource with tags with django and DRF
Recently I was working on new exciting part of syncano-platform and I wanted to create a piece of api that would allow for filtering resources with many to many relationships.
I wanted to have a resource that can be tagged.
Adding such tags should be RESTful and straightforward. Also I wanted to add the ability to search using tags. My go to Rest library is Django Rest Framework.
Currently the most popular solution for tagging in django is django-taggit, however it doesn't integrate with Django Rest Framework well and heavily uses ContentTypes. In my current project at work we don't use ContentTypes at all.
It's also a pretty old library and has support for old django, south migrations, stuff that we just don't need in new projects.
I've read in a blog post about django-taggit:
Tags can be hard if you try to implement them yourself, so don't try and just use this library from the start.
And well, I disagree with this quote, how hard can it can be?
I sometimes prefer to write a simple code, well tailored to my use case, that install another library with all its dependencies. If library is poorly written you'll waste a lot of time debugging its strange behaviors and fighting with it when trying to tailor it your custom requirements. (I'm looking at you python social auth)
I decided to just do it myself. We will now distill this experience by building a simple app with two resources and REST API for them. Here is a code for a whole app.
App Ingredients
- python 3.4
- django 1.8
- Django Rest Framework 3.1
- django-filter
- django-cors-headers 1.1.0
For simplicity I will use sqlite as a DB.
Data models
Let's imagine that we're building a very simplified version of PYPI. We're indexing libraries, we store their names, websites, descriptions and tags.
User can then search libraries using names, descriptions and tags, they can even add new tags to them.
How we will build it? We will have two resources
- tags
- libraries
In django, models are defined in models.py
models.py
from django.db import models
class Tag(models.Model):
"""Tag for data. Every tag has unique text.
"""
text = models.CharField(max_length=64, unique=True)
def __str__(self):
return 'Tag[id: {id}, text: {text}]'.format(
id=self.id, text=self.text)
class Library(models.Model):
"""Library represents a piece of software. It has its website url,
name, description and as many tags as you like.
"""
# django has a nice field that validates URLs
url = models.URLField()
# name should be in a slug form
name = models.SlugField(max_length=80)
description = models.CharField(max_length=256, blank=True)
tags = models.ManyToManyField(Tag, related_name='libraries')
def __str__(self):
return 'Library[id: {id}, name: {name}]'.format(
id=self.id, name=self.name)
And we will create nice RESTy api for them. It will be possible to add tags to libraries and vice versa.
To use django rest framework with those models, we need at three things:
- serializers
- views
- routing
I usually start with serializers, serializer will translate our models to
serialized date (it can be other way round too). I'm going to use simple
serializers based on models directly. DRF has ModelSerializer
s for that.
serializers.py
from rest_framework import serializers
from .models import Library, Tag
class TagSerializer(serializers.ModelSerializer):
libraries = serializers.SlugRelatedField(
many=True,
read_only=True,
slug_field='name'
)
class Meta:
model = Tag
class LibrarySerializer(serializers.ModelSerializer):
tags = serializers.SlugRelatedField(
many=True,
queryset=Tag.objects.all(),
slug_field='text'
)
class Meta:
model = Library
The only tricky part there was to define SlugRelatedField
s correctly. You can
read more about related fields in DRF
here.
With serializers in place, we can move to views part. Views let our app to communicate with the rest of the world.
In DRF you can either use views or viewsets. Views are simpler to understand, because they have the same methods as http verbs. Viewsets simplify lots of things and they are on higher abstraction level, so instead of post, you have create method.
Also viewsets combine very well with routers and usually using viewset results in using much less code.
Here I will use ModelViewSet
s, that gives us full CRUD functionality for free.
views.py
from rest_framework import viewsets
from .models import Library, Tag
from .serializers import LibrarySerializer, TagSerializer
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
class LibraryViewSet(viewsets.ModelViewSet):
queryset = Library.objects.all()
serializer_class = LibrarySerializer
Simple, isn't it?
Now, let's connect views to urls.
urls.py
from rest_framework.routers import DefaultRouter
from .views import LibraryViewSet, TagViewSet
router = DefaultRouter()
router.register(r'libraries', LibraryViewSet)
router.register(r'tags',TagViewSet)
urlpatterns = router.urls
That's actually all we need to create DRF browsable API for those resources.
Adding filtering
We can add new things now, but finding something is currently a pain.
Let's implement filtering! We will enable filtering of libraries by:
- tags
- name
- description
DRF has a pretty good support for filtering. You can read more about it here.
Even though support is good, we still have to do some things:
- create a custom filter
- enable filtering Backend
Let start from creating a custom filter. We will put in in separate file, so it will be easier to find.
filters.py
import django_filters
from .models import Library, Tag
class LibraryFilter(django_filters.FilterSet):
# default for CharFilter is to have exact lookup_type
name = django_filters.CharFilter(lookup_type='icontains')
description = django_filters.CharFilter(lookup_type='icontains')
# tricky part - how to filter by related field?
# but not by its foreign key (default)
# `to_field_name` is crucial here
# `conjoined=True` makes that, the more tags, the more narrow the search
tags = django_filters.ModelMultipleChoiceFilter(
queryset=Tag.objects.all(),
to_field_name='text',
conjoined=True,
)
class Meta:
model = Library
fields = ['name', 'description', 'tags']
To create a custom filter, we had to inherit after django_filters.FilterSet, define our model in Meta and define how each of filters should behave.
django_filters offers few types of lookup_type
s for example: exact
- that
would match an exact string, 'contains', that would returns matches that contain
given string, you can add i
before those lookup_types, to make them ignore
case.
The only tricky part is filtering by related field using its field, different than foreign key. I've seen some examples with filtering by related fields using foreign key, but I couldn't find in any docs, how to do it with different field (in our case - tag text).
Eventually I had to dig into source of django fields
to find it out. It's enough to set to_field_name='field_name'
and it should work.
To connect filter with view, you have to add following too lines to the view:
filter_backends = (filters.DjangoFilterBackend,)
filter_class = LibraryFilter
You can also set filtering backend in django app settings if you like.
Here is how it should look like in context:
views.py
from rest_framework import filters, viewsets
from .filters import LibraryFilter
from .models import Library, Tag
from .serializers import LibrarySerializer, TagSerializer
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
class LibraryViewSet(viewsets.ModelViewSet):
queryset = Library.objects.all()
serializer_class = LibrarySerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_class = LibraryFilter
Using our API
You can now list all tags:
$ curl http://127.0.0.1:8000/tags/
Here is how result will look like (if you've created some data before):
[
{
"text": "python",
"libraries": [
"django",
"django-rest-framework"
],
"id": 1
},
{
"text": "web",
"libraries": [
"django",
],
"id": 2
}
]
If you curl libraries:
$ curl http://127.0.0.1:8000/libraries/
your result will look like this:
[
{
"description": "cool web framework",
"name": "django",
"url": "https://www.djangoproject.com/",
"tags": [
"web",
"python"
],
"id": 1
},
{
"description": "api framework",
"name": "django-rest-framework",
"url": "http://www.django-rest-framework.org/",
"tags": [
"python"
],
"id": 2
}
]
You can now perform queries with filters like below!
$ curl http://127.0.0.1:8000/libraries/?name=rest&tags=python
[
{
"description": "api framework",
"name": "django-rest-framework",
"url": "http://www.django-rest-framework.org/",
"tags": [
"python"
],
"id": 2
}
]
Pretty neat!
Summary
I hope that you'll find this blog post useful. Building an APIs can actually be a pretty complex task, because there are so many components that have to be connected.
Also, we've just scratched a surface. Our API doesn't have proper authorization, paging, ordering, permission system or tests, we also have pretty scarce validation, no api versioning and we only handle JSON.
You can download or read full code on github.