-
Notifications
You must be signed in to change notification settings - Fork 1
Django REST Framework
Representational State Transfer (REST) is a web architecture that defines how to interact with content on the web. When you typically browse the web through a browser, you access a URL and retrieve a HTML file in return. With REST, you also access a URL, but can do so using a set of operations, e.g. that we want to create an object, update it, etc. Furthermore, data sent between you and the server is sent in a raw format, which means the data has been minimally encoded. In our system, we interchange data using the JavaScript Object Notation (JSON) format. See the link at the bottom of the page to learn how JSON works.
By creating a set of URLs supporting certain REST operations (called endpoints, shortened as EP), we have effectively created an application programming interface, or API for short. An API is an alternative way to communicate with our web server without going through the web browser. The EPs define what operations we allow users to utilize. If we choose to make our API publically available, external developers can create applications that communicate with our web application.
Django does not support REST out of the box. However, we can use a popular plugin for this purpose: Django REST Framework (DRF). DRF provides 2 main additions to django:
- Make django views REST compatible
→ We can use the views.py file to define our REST operations, how we should process the data, and how to return it. - Add support for serializing and deserializing data
→ In order to make the JSON data received on the server usable in django, we need to convert it into python datatypes. For this purpose, we use a deserializer. For the opposite direction, when we want to return data to the user in JSON, we use a serializer. DRF allows us to define deserializers and serializers that can be used in our views. This process is similar to how we handle forms in (vanilla) django. In the next section we'll explore a bit more how to use deserializers and serializers in practice.
The KSG-nett API currently only deals with economy related stuff, hence why it's often dubbed the X-API. It is contained within the api
app folder in our project. The API documentation is available by going to localhost:8000/docs.
Let's see how to integrate DRF into our project.
DRF makes vanilla django urls work like EPs. The only thing we need to consider is making our URL patterns more "RESTful", i.e. constructing them in a way where we specify a resource that we can perform an operation on. Consider the example url pattern from the previous section:
urlpatterns = [
path('my-view/', my_view)
]
In order to make this more RESTful, we should try to access a specific resource instead of the basic view:
urlpatterns = [
path('views/<int:view_id>', my_view)
]
Just from reading this url pattern, we can expect that the database contains a number of views
, and that this EP accesses a specific one by providing an id, like this: https://example.com/views/2
. The id corresponds to the primary key of the object in the database as default, but this can be changed later on.
Now that we have accessed an object through an EP, we can do something to it by providing an HTTP method. Previously, we have looked how forms utilize GET and POST. In addition, there exists a handful more, but for the API in this project you generally only have use for:
-
GET
: Retrieving an object or a list of objects -
POST
: Creating a new object in the database -
PUT
: Adding new field(s) to an existing object -
PATCH
: Modifying existing field(s) to an existing object -
DELETE
: Deleting an object from the database
In most cases, all of these methods, except POST, expect the EP to include an id. Since POST creates new objects, the database will choose an id for us automatically. However, there are exceptions to this rule, where you don't follow these conventions on purpose due to the way you've built your API.
Whenever we request something from a web server and receive a response, it's usually together with a status code. Examples of status codes are 200, 404 and 500. The purpose of this 3 digit code is to tell us the outcome of the request, and usually includes a short description, e.g. 200 OK. Status codes range from 100 through 500, where the rule is that the 100 range contains request information, 200 range contains success messages, 300 range contains redirections, 400 range contains user/client errors, and the 500 range contains server errors.
In REST, some status codes are connected to certain operations. The most common are:
-
200 OK
- Information retrieved from GET successfully, or PATCH/PUT updated correctly -
201 CREATED
- POST request created a new object successfully -
204 NO CONTENT
- DELETE request removed object successfully -
400 BAD REQUEST
- General error when client/user has sent a wrong request -
401 UNAUTHORIZED
- Client has not logged in (authenticated) -
403 FORBIDDEN
- Client has logged in, but does not have access to perform this request -
404 NOT FOUND
- Requested resource does not exist -
500 INTERNAL SERVER ERROR
- Something went wrong on the server
Adding DRF to an existing django project can be done without much modifications to existing views. However, to both get more control over what our views do, and to make the code more human readable, we have decided to create our API views as class based views. A class based view imlements a python class containing all view logic, and a python function for each operation we want to enable. In addition, we utilize DRFs generic views which implement all REST functionality for us. Here's an example:
from app.serializers import ObjectSerializer
from rest_framework.generics import ListAPIView, UpdateAPIView, CreateAPIView
class GetView(ListAPIView):
serializer_class = ObjectSerializer
queryset = Object.objects.all()
def get(self, request, *args, **kwargs):
unserialized_object = self.get_queryset()
serializer = self.get_serializer(unserialized_data, many=True)
serialized_data = serializer.data
return Response(serialized_data, status=status.HTTP_200_OK)
class PatchView(UpdateAPIView):
queryset = Object.objects.all()
lookup_url_kwarg = 'object_id'
def patch(self, request, *args, **kwargs):
requested_object = self.get_object()
deserializer = ObjectDeserializer(requested_object, data=request.data, partial=True)
deserializer.is_valid()
deserializer.save()
return Response(status=status.HTTP_200_OK)
class PostView(CreateAPIView):
def post(self, request, *args, **kwargs):
deserializer = ObjectDeserializer(data=request.data)
deserializer.is_valid()
new_object = deserializer.save()
return Response(status=status.HTTP_201_CREATED)
What we are doing in these views is extending the appropirate generic view wrt. the required HTTP methods. In the first view we need GET, so ListAPIView
is chosen. In the second we need PATCH, so we choose UpdateAPIView
. And in the third POST, thus CreateAPIView
. Further, we can define some common constants at the top, and finally implement the logic for each HTTP method.
To explain the method implementations, we need to understand how the serializer and deserializer works.
A serializer should input some django object and return raw data, e.g. JSON. We can define our serializer to be used across the view in the serializer_class
constant. In addition, we set the queryset we want to fetch objects from in the queryset
constant. We access these constants through predefined methods, such as get_queryset
and get_serializer
. What we need to do in this case is input our objetcs into the serializer and specify if it contains multiple objects or not. Then we can serialize the object into JSON by calling .data
. The data is then safe to return to the client.
The serializer implementation, which is placed in the file serializers.py
, will look like this:
from rest_framework import serializers
class ObjectSerializer(serializers.Serializer):
name = serializers.CharField(read_only=True)
text = serializers.CharField(read_only=True)
number = serializers.IntegerField(read_only=True)
A deserializer works in the opposite direction of a serializer, namely transforming input JSON data into a django object. The usage of deserializers differ when creating or updating objects. When creating, you only input the request data. When updating, you need to do the following:
- First fetch the object using the
get_object
method, which also requires thelookup_url_kwarg
constant to be set to what the URL id variable is set to. Input this into the deserializer. - Then input the request data.
- Then tell the deserializer if you should partially update the object, i.e. if you are updating only some of the objects fields. If you are replacing all fields, you should not include this.
After instantiating the deserializer, you should validate the input data by using the
is_valid
method. The validator ensures that the object can be created/updated successfully, and will return an error otherwise. If the validator passes, you cansave
the deserializer, which will write the changes to the database. Optionally, you can then serialize the new object and return it back to the client.
A deserializer typically looks just like a serializer, except the fields are writeable. In addition, we can include custom validators and create/update mechanisms.
from rest_framework import serializers
class ObjectDeserializer(serializers.Serializer):
name = serializers.CharField()
text = serializers.CharField()
number = serializers.IntegerField()
def validate_name(self, value):
if value == "Tormod":
raise serializers.ValidationError("There can only be one Tormod")
return value
def create(self, validated_data):
new_object = Object(**validated_data)
new_object.save()
do_some_sneaky_stuff(new_object)
return new_object
The API employs an autodoc plugin that automatically generates API documentation for us based on the serializer and deserializer implementation for each EP. Sometimes we need to help the autodocumenter a bit by decorating our views. This is done by instantiating the swagger_auto_schema
function with the custom documentation, and calling it on the view we want documented, like this:
from drf_yasg.utils import swagger_auto_schema
swagger_auto_schema(
method='get',
operation_summary="Retrieve list of objects",
responses={
200: ": Objects retrieved",
},
)(MyView.as_view())
The decorated code is placed inside the file decorators.py
, where you can find more examples on how to decorate your views. For the full range of documentation functionality, please view the DRF YASG documentation at the bottom of this page.
Learn more:
- Learn how JSON works
-
DRF tutorial (click
Next ->
in the toolbar to continue the tutorial) - Official DRF documentation
- HTTP status dogs (status code reference)
- DRF YASG documentation