Django REST Framework - getfutureproof/fp_guides_wiki GitHub Wiki
So far you've learned how to make a Django app entirely in Python using templating to display your data on HTML pages with Django's template language. This is a great way to build an app but what if we want to make this project into an API, which can be called upon for data to be displayed in a different front-end such as React.js?
📣 Introducing Django REST framework. 📣
Please do take a look at the documentation to familiarise yourself with all the amazing features, but read on for a quick start guide to turn the Shelter Project from the Django Tutorial into an easily accessible API.
Make sure you are in the root of your project folder and run: pipenv install djangorestframework
.
You should then add it to your installed apps:
#shelter/settings.py
INSTALLED_APPS = [
...
'rest_framework',
...
]
I like to add it between my custom apps and those that come with Django but feel free to add it anywhere, it doesn't matter.
Next we are going to remove files that we will no longer be needing which includes any templates
and forms
.
Finally let's alter our urls to reflect those of an API and remove the existing auth routes for now, which we will add back in later.
#shelter/urls.py
urlpatterns = [
path('admin/', admin.site.urls),
path('api/dogs/', include('adoption.urls')),
]
These help us in our pursuit of an API which can provide and receive data in a format easy to work with such as JSON
, acting as coverters. Serializers also have the added benefit of validating data, much like a form. Read more about them here.
Start by creating a new file adoption/serializers.py
.
# adoption/serializers.py
from rest_framework import serializers
from .models import Dog
class DogSerializer(serializers.ModelSerializer):
class Meta:
model = Dog
fields = ('id', 'name', 'breed', 'owner')
As you can see we are extending the model serializer to make one of our own, based on the Dog model we previously created.
The fields match these, determining what data will be passed on, adding in the id
which is useful for dynamic displays on the front-end.
With our serializer created, we can now tell the views.py
to use this rather than templates. To do this we are going to use Django REST framework's class based API view.
Learn more about Django Class Based Views.
# adoption/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
...
from .serializers import DogSerializer
class DogList(APIView):
def get(self, request, format=None):
dogs = Dog.objects.all()
serializer = DogSerializer(dogs, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = DogSerializer(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)
As you can see we are defining the GET
and POST
requests within the APIView
, this replaces the need for conditionals as it is built in to the view.
For the GET
function we still need to get all dogs, but this time pass them to the DogSerializer
and return this instead of a template. As there will likely be more than one dog, we also need to let the serializer know that.
In the POST
function we use the serializer to validate the data before saving, and return error status that reflect the outcome.
The last thing we have to do in order to finish our first API route is update our urls.
# adoption/urls.py
from django.urls import path
from .views import DogList
urlpatterns = [
path('', DogList.as_view()),
]
Let's check that is working by running python manage.py runserver
and navigating to http://127.0.0.1:8000/api/dogs/
.
We can also easily add to our dog list:
As well as seeing all of our dogs, we also may want to view individual ones. This DetailView
is also a good place to be able to edit or delete.
It's worth checking the object exists first too and if not raising an error.
# adoption/views
...
from django.http import Http40
...
class DogDetail(APIView):
def get_object(self, dog_id):
try:
return Dog.objects.get(pk=dog_id)
except Dog.DoesNotExist:
raise Http404
def get(self, request, dog_id, format=None):
dog = self.get_object(dog_id)
serializer = DogSerializer(dog)
return Response(serializer.data)
def put(self, request, dog_id, format=None):
dog = self.get_object(dog_id)
serializer = DogSerializer(dog, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, dog_id, format=None):
dog = self.get_object(dog_id)
dog.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
Don't forget to add this new view to the urls!
# shelter/urls.py
from rest_framework.urlpatterns import format_suffix_patterns
from .views import DogList, DogDetail
urlpatterns = [
path('', DogList.as_view()),
path('<int:dog_id>/', DogDetail.as_view())
]
urlpatterns = format_suffix_patterns(urlpatterns)
At this point we can also add in the format_suffix_patterns
method. All this does is allow users to receive the data in a format they want by adding a suffix such as .json
to the endpoint.
Have a look at the documentation here.
Once again let's check if it works, this time at http://127.0.0.1:8000/api/dogs/16
One of the ways in which Django can help speed up development is by giving us some frequently used functionality for free, such as authentication.
We are going to take advantage of this situation by using Django's built in auth views for Login and Logout. It's as simple as adding the below to the list of urls - you can make the endpoint anything that makes sense for your app.
# shelter/urls.py
urlpatterns = [
...
path('api/auth/', include('rest_framework.urls'))
]
As you will see in the top right corner we now have the ability to login.
With authentication added, it's time to protect some of our routes.
# adoption/views.py
from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS
...
class ReadOnly(BasePermission):
def has_permission(self, request, view):
return request.method in SAFE_METHODS
class DogList(APIView):
permission_classes = [IsAuthenticated|ReadOnly]
...
class DogDetail(APIView):
permission_classes = [IsAuthenticated]
...
As you can see from the imports, we can easily stop non-logged-in users from interacting with our data thanks to Django REST framework's IsAuthenticated
class. This is useful but we may also want to entice users to register for our site by giving an idea of some of the data we have to offer which is why it's a good idea to build a ReadOnly
class, extending the BasePermission
. Once that is done, we just have to let the views know which class to use and now our routes are protected.
The last piece of the puzzle is registration. Although we can add users through the admin console, we might want people to have the ability to sign up on their own, so we have to create a register view and serializer.
# users/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
class UserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
password_confirmation = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ('username', 'password', 'password_confirmation')
write_only_fields = ('password', 'password_confirmation')
def create(self, validated_data):
user = User.objects.create(
username=validated_data['username']
)
password=validated_data['password']
password_confirmation=validated_data['password_confirmation']
if password != password_confirmation:
raise serializers.ValidationError({'password': 'Passwords must match.'})
user.set_password(password)
user.save()
return user
There are a few points to note here:
- We have to protect passwords by ensuring they are
write_only=True
, this includes telling the serializer to never show these fields. - We have to use a
CREATE
rather thanPOST
method when dealing with making new users, as this gives us access to thevalidated_data
andset_password
methods. - Adding in a password_conformation is not required, but a best practice for good user experience.
Now we can update our view.
# users/views.py
from rest_framework.views import APIView
from django.contrib.auth.models import User
from rest_framework.response import Response
from rest_framework import status
from .serializers import UserRegistrationSerializer
class UserRegistrationView(APIView):
def post(self, request, format=None):
serializer = UserRegistrationSerializer(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)
This is fairly straight forward, and similar to our dog views as the serailizer is doing the hard work.
Lastly we just need to add this to our urls.
# users/urls.py
from django.urls import path
from .views import UserRegistrationView
urlpatterns = [
path('', UserRegistrationView.as_view()),
]
# shelter/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/dogs/', include('adoption.urls')),
path('api/auth/', include('rest_framework.urls')),
path('api/register/', include('users.urls')),
]
And that's it - now you can add the front-end of your choice!