35. Building a RESTful web service in Django

We’re going to return to the bioscience app that we’ve been developing and create a RESTful API for the data.

35.01 Introduction to serialization

Our REST API is a web service. What it’s going to do is serve data – it’s going to get data out of the database, turn them into JSON objects and send those objects back to the user.

The act of turning the data into JSON is serialization. Thankfully, most web frameworks come with serializer/deserializer code that allows us to convert our database objects into strings to send to the user, or take user JSON strings, and turn them into Python data structures that we can insert in the database.

We could write that code ourselves. It’s not hugely complicated, but it’s quite fiddly, and it’s been done before by many other people.

35.02 Coding – setup

A reminder for those using Windows 10 on locating and restarting the project files, using these shell commands after navigating to the desktop:

$ cd bioweb
$ python -m venv env
$ env\Scripts\activate

We may need to insall the Django REST framework:

(env) $ pip install djangorestframework

Then to run the server:

(env) $ cd bioweb
(env) $ python manage.py runserver

If we now go to http://127.0.0.1:8000/ in our browser we can see our project.

We can put all our API code inside views.py, but it is good to create a new file called api.py which we can create inside bioweb/genedata. We can also create a new file called serializers.py here too, which will contain the code which will serialize/deserialize the data to and from JSON.

The serializers in REST framework work very similarly to Django’s Form and ModelForm classes. The two major serializers that are most popularly used are ModelSerializer and HyperLinkedModelSerializer.

We’ve installed the Django REST framework so we need to add this to our installed apps in settings. So go to settings.py and add this:

INSTALLED_APPS = [
    'rest_framework',
    ... (all the other apps already here)
]

We additionally need to add a new namespace for the framework. So under the INSTALLED APPS = [] section add this in settings.py:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

35.03 Implementing the GET endpoints

If we look at models.py and remind ourselves of the models (tables) we’ve created, the central one is the Gene table. So we’re going to build an API that serves this data to users.

Let’s start by defining the serializers, by going to serializers.py. We need to import a package from the REST framework and also import our data models as well:

from rest_framework import serializers
from .models import *

There are two different ways we can do this. We can either write the code manually, or use some shortcuts. This is how to do it manually:

We write a class called GeneSerializer which inherits from serializers.Serializer. We then define class variables for each field in the table that we want to server to users.

class GeneSerializer(serializers.Serializer):
    gene_id = serializers.CharField(required=True, allow_blank=False, max_length=256)
    entity = serializers.CharField(required=True, allow_blank=False, max_length=256)
    start = serializers.IntegerField()

The serializer will check incoming data to make sure it’s correct.

It would be quite time-consuming to build up serializers each way, especially if we have lots more class variables from fields to create, so we can use the shortcuts – making use of model serializers instead. Instead of the class above, we could do this:

class GeneSerializer(serializers.ModelSerializer):
    class Meta:
        model = Gene
        fields = ['gene_id', 'entity', 'start', 'stop', 'sense', 'start_codon']

This uses the Meta class and defines a model and the fields to use in that class. Note that here we inherit from serializers.ModelSerializer. Doing it this way helps us write a lot less code.

Now we can turn to api.py. Firstly we need to import various packages to implement our first endpoint:

from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from .models import *
from .serializers import *

We will look at @crsf later but for now we will say it’s exempt from a security check.

@csrf_exempt
def gene_detail(request, pk):
    """
    Retrieve, update or delete a code snippet.
    """
    try:
        gene = Gene.objects.get(pk=pk)
    except Gene.DoesNotExist:
        return HttpResponse(status=404)
    if request.method == 'GET':
        serializer = GeneSerializer(gene)
        return JsonResponse(serializer.data)

When we are serving the gene we want the request from the user and the gene ID (pk). The try/expect part shows we try to get an object from the Gene table and run an exception if this fails, with a 404 response.

Then we check that if the user made a GET request and use the serializer that we just wrote. We then return it to the user.

We now need to wire this up in our urls.py to make it work. So in urls.py we add this code:

from . import api

And in urlpatterns:

path('api/gene//', api.gene_detail),

Save all, make sure the server is running. Did it work? If we go to the main page and click on Gene 5 we go to this URL (Gene 5 in our example has a primary key/ID of 6):

http://127.0.0.1:8000/gene/6

But if we go here instead:

http://127.0.0.1:8000/api/gene/6

You’ll see the JSON string returned, so it works!

{"gene_id": "Gene5", "entity": "Plasmid", "start": 786, "stop": 888, "sense": "U", "start_codon": "M"}

35.04 Implementing POST

We now want to implement the REST endpoint that allows our users to upload data into our database for the gene model. Users will be sending data using the HTTP POST method.

The first thing we will do is a little bit of refactoring. If we open up api.py we will need to import a few more packages:

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status

We only had one function originally in this file. We can write a second function too, genes_list, to return an API of all the genes.

To link this up we need to add this to serializers.py:

class GeneListSerializer(serializers.ModelSerializer):
    class Meta:
        model = Gene
        fields = ['gene_id', 'entity', 'start', 'stop', 'sense', 'start_codon']

And this to urlpatterns in urls.py:

path('api/genes/', api.genes_list),

Then we can rewrite the two functions as follows:

@api_view(['GET', 'POST'])
def gene_detail(request, pk):

    if request.method == 'POST':
        serializer = GeneSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    try:
        gene = Gene.objects.get(pk=pk)
    except Gene.DoesNotExist:
        return HttpResponse(status=404)
    if request.method == 'GET':
        serializer = GeneSerializer(gene)
        return Response(serializer.data)

@api_view(['GET'])
def genes_list(request):
    if request.method == 'GET':
        gene = Gene.objects.all()
        serializer = GeneListSerializer(gene, many=True)
        return Response(serializer.data)

If we now look at http://127.0.0.1:8000/gene/6 we can see the JSON for Gene5 with the foreign keys added.

Also if we now look at http://127.0.0.1:8000/api/genes/ we will get a JSON with a list of genes.

In gene_detail we are checking to see if we had a POST method from the user. We’re passing data coming from the user into serializer, checking if it is_valid() and then saving it if it is.

We can then tell the user it was a success or a failure using the 201 and 400 codes.

Note that we’re using @api_view – gene_detail will respond to GET or POST, but gene_list will only respond to GET.

If we look in our gene class there are two foreign keys and serializers.py needs to handle these as well.

class ECSerializer(serializers.ModelSerializer):
    class Meta:
        model = EC
        fields = ['id', 'ec_name']

class SequencingSerializer(serializers.ModelSerializer):
    class Meta:
        model = Sequencing
        fields = ['id', 'sequencing_factory', 'factory_location']

We now need to use these two serializers in our gene serializer in order to link this data together, and that’s quite easy. We can just make a instance of each class, and then add them to the list of fields.

class GeneSerializer(serializers.ModelSerializer):
    ec = ECSerializer()
    sequencing = SequencingSerializer()
    class Meta:
        model = Gene
        fields = ['gene_id', 'entity', 'start', 'stop', 'sense', 'start_codon', 'ec', 'sequencing']

The data that the user is going to provide for sequencing and EC, such as sequencing factory, factory location, that’s not stored in the Gene table. We need a way to handle this data.

We are going to overwrite the serializer’s Create method, so when you call save on a serializer, it calls the create method that does the actual database saving so it creates the database record.

We get a copy of the request object and a copy of the validated the data. If it passed validation the data is considers to be valid. We’ll take a copy of the EC data that the user provides, and a copy of the sequencing table data as well.

So our class with the new function now looks like this:

class GeneSerializer(serializers.ModelSerializer):
    ec = ECSerializer()
    sequencing = SequencingSerializer()
    class Meta:
        model = Gene
        fields = ['gene_id', 'entity', 'start', 'stop', 'sense', 'start_codon', 'ec', 'sequencing']
    
    def create(self, validated_data):
        ec_data = self.initial_data.get('ec')
        seq_data = self.initial_data.get('sequencing')
        gene = Gene(**{**validated_data, 
            'ec' : EC.objects.get(pk=ec_data['id']), 
            'sequencing' : Sequencing.objects.get(pk=seq_data['id'])
        })
        gene.save()
        return gene

We can now take a new chunk of JSON, let’s say for Gene6 which we don’t currently have in our database.

{
    "gene_id": "Gene6",
    "entity": "Chromosome",
    "start": 234,
    "stop": 456,
    "sense": "+",
    "start_codon": "M",
    "ec": {
        "id": 1,
        "ec_name": "oxidoreductase"
    },
    "sequencing": {
        "id": 1,
        "sequencing_factory": "Sanger",
        "factory_location": "UK"
    }
}

Further reading

This is a useful quickstart guide: https://www.django-rest-framework.org/tutorial/quickstart/

Wednesday 24 November 2021, 13 views


Leave a Reply

Your email address will not be published. Required fields are marked *