Let’s get started with django rest api framework as we saw in the last blog.
First we need to install django rest api framework. Here are the steps
$ virtualenv env
$ source env/bin/activate
$ pip install django
$ pip install djangorestframework
$ django-admin startproject restapi
$ cd restapi
$ python manage.py migrate
$ django-admin startapp todo
$ python manage.py createsuperuser --email [email protected] --username admin
After these steps, rest api framework should be activated.
Reference
https://www.django-rest-framework.org/tutorial/quickstart/#project-setup
Also, once important thing to realize is that “DRF” is just a plugin on top of django. So everything related to django still works.
Serializers
Serializers is an important concept to understand. In django we have models to define our data structure. The problem we face is that, to return data from model in api in json format or any other format there is no way to do it. What serializes do is basically provide a way convert django models to different format’s like string, json, xml etc.
Before looking at how to code serializers its important to understand what they are. They are just a way to convert django model’s to different formats
Reference
https://www.django-rest-framework.org/api-guide/serializers/#serializers
Now let’s see this in action. First we will define a simple todo model
from django.db import models
# Create your models here.
class Todo(models.Model):
task = models.CharField(max_length=255)
Let’s create a route to return a list of “Todo” follow basic steps to create a route which we have already seen in django part 1. so right now my route looks like this
from django.shortcuts import render
# Create your views here.
from django.http import HttpResponse
# Create your views here.
from todo.models import Todo
def index(request):
todo = Todo.objects.all()
print(todo)
return HttpResponse("Hello, world!")
First i need to insert some data, we will put in some random data for now. Creating a dummy route to insert data
//todo/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('dummy', views.dummy, name='dummy'),
]
//todo/views.py
from django.shortcuts import render
# Create your views here.
from django.http import HttpResponse
# Create your views here.
from todo.models import Todo
def index(request):
todo = Todo.objects.all()
print(todo)
return HttpResponse("Hello, world!")
def dummy(request):
todo = Todo(task="dummy todo")
todo.save()
return HttpResponse(todo.id)
There are few basic steps to do here which need to be done when setting up a new app "todo" in django
This is not part of DREF but still mentioning it here
Go to your main project urls.py file and add
path('todo/', include('todo.urls')),
next go to your main project settings.py and add under INSTALLED_APPS
'todo.apps.TodoConfig'
next run command
$ python manage.py makemigrations
$ python manage.py migrate
Next run server using $python manage.py runserver
Now fire the “dummy” route first in postman so that few data gets inserted.
Now, if we fire this route in our browser as we can see on our terminal. This is what shows up
<QuerySet [<Todo: Todo object (1)>, <Todo: Todo object (2)>]>
This format is of no use, we need json data. This is where is serializes come into picture
First define a simple serializer
//todo/serializers.py from todo.models import Todo from rest_framework import serializers class TodoSerializer(serializers.ModelSerializer): class Meta: model = Todo fields = ('id', 'task')
Next in our route we will use it like this
def index(request): todo = Todo.objects.get(pk=1) ser = TodoSerializer(todo) print(ser.data) return HttpResponse("")
Now in terminal we see output like this
So data got transformed to json.
Django Shell
Django also offers a “shell” for dev’s to test our code quickly. For instance, above we want to just how model serializers work, but we had to define a full route to see the output and use postman to fire it. That not a good practices when we want to test out things in django. We should use shell, where we can write small code snippets and see there output.
If we are to test the same above in django shell, lets see how that would be
$ python manage.py shell
$ from todo.models import Todo
$ todo = Todo.objects.get(pk=1)
$ ser = TodoSerializer(todo)
$ ser.data
$ import io
$ from rest_framework.renderers import JSONRenderer
$ content = JSONRenderer().render(ser.data)
$ content
Let’s continue further with serializers are there are lot of things understand in this
Let’s create more complex models
from django.db import models
from datetime import datetime
# Create your models here.
class Todo(models.Model):
task = models.CharField(max_length=255)
description = models.TextField(default="")
priority = models.IntegerField(default=1)
created = models.DateTimeField(default=datetime.now,blank=True)
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('id', 'task','description','priority','created')
$ python manage.py makemigrations todo
$ python manage.py migrate
$ python manage.py shell
>>> from todo.models import Todo
>>> from todo.serializers import TodoSerializer
>>> todo = Todo(task="123")
>>> todo.save()
>>> ser = TodoSerializer(todo)
>>> ser.data
{'id': 4, 'task': '123', 'description': '', 'priority': 1, 'created': '2019-04-27T06:32:11.238866Z'}
Look’s good, lets see further. and try to fetch a list of Todo and use serializer
>>> todos = Todo.objects.all()
>>> ser = TodoSerializer(todos,many=True)
>>> ser.data
[OrderedDict([('id', 1), ('task', 'dummy todo'), ('description', ''), ('priority', 1), ('created', '2019-04-27T06:31:37.567277Z')]), OrderedDict([('id', 2), ('task', 'dummy todo'), ('description', ''), ('priority', 1), ('created', '2019-04-27T06:31:37.567277Z')]), OrderedDict([('id', 3), ('task', 'dummy task1'), ('description', ''), ('priority', 1), ('created', '2019-04-27T06:31:37.567277Z')]), OrderedDict([('id', 4), ('task', '123'), ('description', ''), ('priority', 1), ('created', '2019-04-27T06:32:11.238866Z')])]
So we just need to use many=True for multiple data
Next let’s see how we can validate data with serializers
To test our validate i will make small changes to model i.e add an email field
class Todo(models.Model):
email = models.EmailField(max_length=70, null=True, blank=False, unique=True)
task = models.CharField(max_length=255)
description = models.TextField(default="")
priority = models.IntegerField(default=1)
created = models.DateTimeField(default=datetime.now,blank=True)
Now in the shell we can try out validations how it works
$ python manage.py makemigrations todo
$ python manage.py migrate
$ python manage.py shell
>>> from todo.serializers import TodoSerializer
>>> ser = TodoSerializer(data={"email":"123"})
>>> ser.is_valid()
False
>>> ser.errors
{'email': [ErrorDetail(string='Enter a valid email address.', code='invalid')], 'task': [ErrorDetail(string='This field is required.', code='required')]}
This way data can be validated.
In the above you would have noticed that we didn’t use Todo model at all. We can extend our serializers to return python object’s directly and do save, update via serializers itself.
e.g in the above code we can do this
>>> ser = TodoSerializer(data={"email":"[email protected]","task":"123"})
>>> ser.is_valid()
True
>>> ser.save()
>>> ser.data
{'id': 5, 'email': '[email protected]', 'task': '123', 'description': '', 'priority': 1, 'created': '2019-04-27T08:42:57.157928Z'}
cool this works!
Another another interesting thing, we can only call “save()” after “is_valid()” is called and return True. If you don’t you will get this error “AssertionError: You must call .is_valid()
before calling .save()
.”
We can also configure the serializer to return instance of the object. What this means is that support we do this
>>> object = ser.save() >>> object {'id': 5, 'email': '[email protected]', 'task': '123', 'description': '', 'priority': 1, 'created': '2019-04-27T08:42:57.157928Z'}
Basically the return value of save() is string i.e serialized data and not the actual object. If we can actual object we need to do this further
from todo.models import Todo
from rest_framework import serializers
# from todo.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = "__all__"
def create(self, validated_data):
return Todo.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.email = validated_data.get('email', instance.email)
instance.task = validated_data.get('task', instance.task)
instance.description = validated_data.get('description', instance.description)
instance.priority = validated_data.get('priority', instance.priority)
instance.created = validated_data.get('created', instance.created)
instance.save()
return instance
Here we have added two function for save/update. Now our serializes will return object’s instead of data. To see the output close your shell and open it again
>>> from todo.serializers import TodoSerializer
>>> ser = TodoSerializer(data={"email":"[email protected]","task":"123"})
>>> ser.is_valid()
True
>>> x = ser.save()
>>> x
<Todo: Todo object (6)>
So now we have a django object instead of string data. This is much better for further operations with models.
Also another thing to note. If i do this i.e put a duplicate email id
>> from todo.serializers import TodoSerializer
>>> ser = TodoSerializer(data={"email":"[email protected]","task":"123"})
>>> ser.is_valid()
False
>>> ser.errors
{'email': [ErrorDetail(string='todo with this email already exists.', code='unique')]}
which is cool! The validation happens at database level as well, not just at the object level.
Before going further with serialize another thing to note is that we are using ” ModelSerializer ” for our class. There are many other serializers and also 3rd party serializer available for different purposes. Open this link and see the left menu for different types
Now, next let’s look at how we can handle relationship’s between model’s and serializers
Let’s define another model “User” and assign “Todo” to user. Before moving further i would like to change our database engine to “mysql”. Till now we are on the default engine “sqlite3”
To make the changes first make sure mysql is installed on your system and create a user and database. For my case i created these details
username: user password: user database: restapi
Next install mysql client for python. i had to do these steps
$ sudo apt install default-libmysqlclient-dev
// activate your virtual env and install
$ pip install mysqlclient
In your settings.py file make the changes to database engine.
previously existing
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
change this to
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'restapi', 'USER': 'user', 'PASSWORD': 'user', 'HOST': 'localhost', 'PORT': '', } }
next run
$ python manage.py migrate
after this all your database tables would be created in mysql.
Next, we can again start off where we left i.e User, Todo models association.