This article is a definitive guide for starters who want to develop projects with RESTful APIs using Python, Django and Django Rest Framework.
This article is a definitive guide for starters who want to develop projects with RESTful APIs using Python, Django and Django Rest Framework.
IntroductionAs described in a dissertion by Roy Fielding,
REST is an "architectural style' that basically exploits the existing technology and protocols of the web.
In simple definition, it is the data representation for a client in the format that is suitable for it.
Hence, RESTful + API is a commonly used terminology for the implementation of such architecture and constraints (eg. in web services).
Here is an example GET request from GitHub's API
$ curl https://api.github.com/users/joshuadeguzman
You will see an output similar to this
{
"login": "joshuadeguzman",
"id": 20706361,
"node_id": "MDQ6VXNlcjIwNzA2MzYx",
"avatar_url": "https://avatars1.githubusercontent.com/u/20706361?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/joshuadeguzman",
"html_url": "https://github.com/joshuadeguzman",
"followers_url": "https://api.github.com/users/joshuadeguzman/followers",
"following_url": "https://api.github.com/users/joshuadeguzman/following{/other_user}",
"gists_url": "https://api.github.com/users/joshuadeguzman/gists{/gist_id}",
"starred_url": "https://api.github.com/users/joshuadeguzman/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/joshuadeguzman/subscriptions",
"organizations_url": "https://api.github.com/users/joshuadeguzman/orgs",
"repos_url": "https://api.github.com/users/joshuadeguzman/repos",
"events_url": "https://api.github.com/users/joshuadeguzman/events{/privacy}",
"received_events_url": "https://api.github.com/users/joshuadeguzman/received_events",
"type": "User",
"site_admin": false,
"name": "Joshua de Guzman",
"company": "@freelancer",
"blog": "https://joshuadeguzman.me",
"location": "Manila, PH",
"email": null,
"hireable": true,
"bio": "Android Engineer at @freelancer. Building tools for humans.",
"public_repos": 75,
"public_gists": 2,
"followers": 38,
"following": 10,
"created_at": "2016-07-28T15:19:54Z",
"updated_at": "2019-06-16T10:26:39Z"
}
Shown above is a data set in JSON format.
JSON or JavaScript Object Notation is an open-standard file format that uses human-readable text to transmit data objects consisting of attribute–value pairs and array data types.
Other formats include XML, INI, CSV, etc. But today, JSON is widely use for its structure is intuitive, making it comfortable to read and map domain objects no matter what programming language is being used.
Python, according to its creator, Guido van Rossum, is a
high-level programming language, and its core design philosophy is all about code readability and a syntax which allows programmers to express concepts in a few lines of code.
Python uses english like words representation (eg. for methods, reserve keywords and control flow) that makes it easier for any beginner to jump right into it. It also features dynamic type system meaning it verifies the type safety of program at runtime. It also does automatic memory management.
print(5 + 5) # This will result to 10
Django is a high-level Python Web Framework that enables developers to deliver projects on time with clean and pragmatic design.
Its flagship features include a design for fast development, a secure and scalable product.
Django's way of propagating changes to your database schema is by means of its migration modules.
Sample User
model
from django.db import models
class User(models.Model):
first_name = models.CharField(max_length=50)
middle_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
def __str__(self):
return self.name
If any changes are made on your models, run makemigrations
$ python manage.py makemigrations
Finally, you can synchronize the database with the set of models and migrations
$ python manage.py migrate
REST APIs with Django Rest Framework
DRF or Django REST Framework is a powerful and flexible toolkit for building Web APIs. It helps the developers to not reinvent the wheel by rolling out complex and solid REST API from scratch by themselves. Because when your projects become more and more complex, you will soon realise the need of using DRF or other helpful rest framework.
Create project directory
$ mkdir djangoapi
Install virtualenv via pip
A virtual environment enables a project to have additional libraries or changes in packages within its environment without disturbing global or libraries of other environments.
pip
is a package management system used to install and manage software packages written in Python.
$ pip install virtualenv
To create an environment folder in your project's directory
$ cd djangoapi
$ virtualenv venv
To activate the environment
$ source venv/bin/activate
To undo these changes to your path, simply run deactivate
. More on virtualenv.
Install django, djangorestframework
$ pip install django
$ pip install djangorestframework
Creating a django project
$ django-admin startproject blog
Running your project
$ python manage.py runserver
System check identified no issues (0 silenced).
You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
August 16, 2018 - 09:58:36
Django version 2.1, using settings 'blog.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
The unapplied migrations refer to the default migration files included when you start a django project.
To synchronize these migration files, simply run migrate
$ python manage.py migrate
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying sessions.0001_initial... OK
The default database in our project is currently set to SQLite named db.sqlite3
.
Creating a django project's app
$ cd blog
$ python manage.py startapp posts
The project structure should look like
$ find .
./posts
./posts/migrations
./posts/migrations/__init__.py
./posts/models.py
./posts/__init__.py
./posts/apps.py
./posts/admin.py
./posts/tests.py
./posts/views.py
./db.sqlite3
./blog
./blog/__init__.py
./blog/__pycache__
./blog/__pycache__/settings.cpython-36.pyc
./blog/__pycache__/wsgi.cpython-36.pyc
./blog/__pycache__/__init__.cpython-36.pyc
./blog/__pycache__/urls.cpython-36.pyc
./blog/settings.py
./blog/urls.py
./blog/wsgi.py
./manage.py
Each model instance is a definitive source of the information about your data. In general, each model pertains to a single table in your database.
# djangoapi/blog/posts/models.py
from django.db import models
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=255)
content = models.TextField()
is_featured = models.BooleanField(default=False)
def __str__(self):
return self.name
__str__
is called by thestr()
built-in function and by the print statement to compute the "informal" string representation of an object.
If you try runningmakemigrations
, django won't see those changes yet.
$ No changes detected
To solve this, add your posts
app to your project's installed apps.
# djangoapi/blog/blog/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'posts' # Add it here
]
To continue with the migration of models
$ python manage.py makemigrations
Migrations for 'posts':
posts/migrations/0001_initial.py
- Create model Post
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, posts, sessions
Running migrations:
Applying posts.0001_initial... OK
Serializers allow data structure or object state to be translated into a format that can be stored or transmitted and be reconstructed later on.
Create API's serializers.py
and views.py
files and isolate them like this
# posts/api
posts/api/serializers.py
posts/api/views.py
# posts/migrations
posts/migrations/
# posts
posts/admin.py
posts/apps.py
posts/models.py
posts/tests.py
posts/views.py
# posts/api/serializers.py
from ..models import Post
from rest_framework import serializers
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('title', 'content', 'is_featured') # if not declared, all fields of the model will be shown
In this tutorial we have used ModelSerializer
, more on this.
A view function, or view for short, is a Python function that takes a Web request and returns a Web response.
# posts/api/views.py
from ..models import Post
from . import serializers
from rest_framework import generics, status
from rest_framework.response import Response
class PostListView(generics.ListAPIView):
queryset = Post.objects.all()
serializer_class = serializers.PostSerializer
As seen above, ListAPIView
is used for read-only endpoints to represent a collection of model instances.
In this code snippet, we use generics
view methods from the rest_framework
, more on this.
This is where we setup our routes or URL paths to our designated views in which we expect specific responses for each.
# posts/urls.py
from django.urls import path
from . import views
from .api import views
urlpatterns = [
path('', views.PostListView.as_view(), name=None)
]
Ensure that the rest_framework
is added to our project's apps.
# djangoapi/blog/blog/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # Add it here
'posts'
]
Since we haven't setup our POST
requests yet, we will be populating the database through django's admin panel.
To do that, create a superuser account admin
with password 1234password
.
$ python manage.py createsuperuser --email [email protected] --username admin
Password:
Password (again):
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
Register the model in the admin panel.
# posts/admin.py
from django.contrib import admin
from .models import Post
# Register your models here.
admin.site.register(Post)
That's it. Visit the admin panel and update posts
model's records. More on this.
$ python manage.py runserver
GET /api/v1/posts/
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[
{
"title": "Example Post #1",
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"is_featured": false
},
{
"title": "Example Post #2",
"content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"is_featured": true
}
]
Great. Now it's time for us to update our views and finish the standard CRUD operations.
POST
is a method used for creating (sometimes updating) a resource in the database.
# posts/api/views.py
from ..models import Post
from . import serializers
from rest_framework import generics, status
from rest_framework.response import Response
class PostCreateView(generics.CreateAPIView):
queryset = Post.objects.all()
serializer_class = serializers.PostSerializer
def create(self, request, *args, **kwargs):
super(PostCreateView, self).create(request, args, kwargs)
response = {"status_code": status.HTTP_200_OK,
"message": "Successfully created",
"result": request.data}
return Response(response)
Most often, we separate List
and Create
view classes when we want to expose a list of data set while easily preventing a certain request to POST
or create a resource in the database for that specific List
view.
Usecase always varies for apps, you are opt to use ListCreateAPIView or even ViewSets for combining the logic for a set of related views.
Optional: Since we want to display the data in a more systematic way, we override create
method and map our inline custom response handler.
Adding more views with methods GET
, PATCH
, DELETE
to handle a specific blog post detail.
class PostDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Post.objects.all()
serializer_class = serializers.PostSerializer
def retrieve(self, request, *args, **kwargs):
super(PostDetailView, self).retrieve(request, args, kwargs)
instance = self.get_object()
serializer = self.get_serializer(instance)
data = serializer.data
response = {"status_code": status.HTTP_200_OK,
"message": "Successfully retrieved",
"result": data}
return Response(response)
def patch(self, request, *args, **kwargs):
super(PostDetailView, self).patch(request, args, kwargs)
instance = self.get_object()
serializer = self.get_serializer(instance)
data = serializer.data
response = {"status_code": status.HTTP_200_OK,
"message": "Successfully updated",
"result": data}
return Response(response)
def delete(self, request, *args, **kwargs):
super(PostDetailView, self).delete(request, args, kwargs)
response = {"status_code": status.HTTP_200_OK,
"message": "Successfully deleted"}
return Response(response)
# posts/urls.py
from django.urls import path
from . import views
from .api import views
urlpatterns = [
path('', views.PostListView.as_view(), name=None),
path('create/', views.PostCreateView.as_view(), name=None),
path('<int:pk>/', views.PostDetailView.as_view(), name=None)
]
Now you can send requests to your API via Postman, your app or do a GET
requests from your browser, examples:
POST /api/v1/posts/create/
HTTP 200 OK
Allow: POST, OPTIONS
Content-Type: application/json
Vary: Accept
{
"status_code": 200,
"message": "Successfully created",
"result": {
"csrfmiddlewaretoken": "rnSUN3XOIghnXA0yKghnQgxg0do39xhorYene5ALw3gWGThK5MjG6YjL8VUb7v2h",
"title": "Creating a resource",
"content": "Howdy mate!"
}
}
GET /api/v1/posts/1/
HTTP 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"status_code": 200,
"message": "Successfully retrieved",
"result": {
"title": "Sample Post",
"content": "Sample Post Content",
"is_featured": false
}
}
That's it. You have successfully managed to develop RESTful APIs with DRF! Cheers!
Source codeAvailable on GitHub.
🔥Intellipaat Django course: https://intellipaat.com/python-django-training/ 👉This Python Django tutorial will help you learn what is django web development &...
This Python Django tutorial will help you learn what is django web development & application, what is django and introduction to django framework, how to install django and start programming, how to create a django project and how to build django app. There is a short django project as well to master this python django framework.
Why should you watch this Django tutorial?
You can learn Django much faster than any other programming language and this Django tutorial helps you do just that. Our Django tutorial has been created with extensive inputs from the industry so that you can learn Django and apply it for real world scenarios.
Best Mobile App Development Company India, WebClues Global is one of the leading web and mobile app development company. Our team offers complete IT solutions including Cross-Platform App Development, CMS & E-Commerce, and UI/UX Design.
We are custom eCommerce Development Company working with all types of industry verticals and providing them end-to-end solutions for their eCommerce store development.
Know more about Top E-Commerce Web Development Company
Master Django models, a core concept of the popular web framework. Dive into advanced topics, like how the Active Record pattern works for Django’s ORM systems
Models are a core concept of the Django framework. According to Django’s design philosophies for models, we should be as explicit as possible with the naming and functionality of our fields, and ensure that we’re including all relevant functionality related to our model in the model itself, rather than in the views or somewhere else. If you’ve worked with Ruby on Rails before, these design philosophies won’t seem new as both Rails and Django implement the Active Record pattern for their object-relational mapping (ORM) systems to handle stored data.
In this post we’ll look at some ways to leverage these philosophies, core Django features, and even some libraries to help make our models better.
getter/setter/deleter
properties
As a feature of Python since version 2.2, a property’s usage looks like an attribute but is actually a method. While using a property on a model isn’t that advanced, we can use some underutilized features of the Python property to make our models more powerful.
If you’re using Django’s built-in authentication or have customized your authentication using AbstractBaseUser
, you’re probably familiar with the last_login
field defined on the User
model, which is a saved timestamp of the user’s last login to your application. If we want to use last_login
, but also have a field named last_seen
saved to a cache more frequently, we could do so pretty easily.
First, we’ll make a Python property that finds a value in the cache, and if it can’t, it returns the value from the database.
accounts/models.py
from django.contrib.auth.base_user import AbstractBaseUser
from django.core.cache import cache
class User(AbstractBaseUser):
...
@property
def last_seen(self):
"""
Returns the 'last_seen' value from the cache for a User.
"""
last_seen = cache.get('last_seen_{0}'.format(self.pk))
# Check cache result, otherwise return the database value
if last_seen:
return last_seen
return self.last_login
Note: I’ve slimmed the model down a bit as there’s a separate tutorial on this blog about specifically customizing the built-in Django user model.
The property above checks our cache for the user’s last_seen
value, and if it doesn’t find anything, it will return the user’s stored last_login
value from the model. Referencing <instance>.last_seen
now provides a much more customizable attribute on our model behind a very simple interface.
We can expand this to include custom behavior when a value is assigned to our property (some_user.last_seen = some_date_time
), or when a value is deleted from the property (del some_user.last_seen
).
...
@last_seen.setter
def last_seen(self, value):
"""
Sets the 'last_seen_[uuid]' value in the cache for a User.
"""
now = value
# Save in the cache
cache.set('last_seen_{0}'.format(self.pk), now)
@last_seen.deleter
def last_seen(self):
"""
Removes the 'last_seen' value from the cache.
"""
# Delete the cache key
cache.delete('last_seen_{0}'.format(self.pk))
...
Now, whenever a value is assigned to our last_seen
property, we save it to the cache, and when a value is removed with del
, we remove it from the cache. Using setter
and deleter
is described in the Python documentation but is rarely seen in the wild when looking at Django models.
You may have a use case like this one, where you want to store something that doesn’t necessarily need to be persisted to a traditional database, or for performance reasons, shouldn’t be. Using a custom property like the above example is a great solution.
In a similar use case, the python-social-auth
library, a tool for managing user authentication using third-party platforms like GitHub and Twitter, will create and manage updating information in your database based on information from the platform the user logged-in with. In some cases, the information returned won’t match the fields in our database. For example, the python-social-auth
library will pass a fullname
keyword argument when creating the user. If, perhaps in our database, we used full_name
as our attribute name then we might be in a pinch.
A simple way around this is by using the getter/setter
pattern from above:
@property
def fullname(self) -> str:
return self.full_name
@fullname.setter
def fullname(self, value: str):
self.full_name = value
Now, when python-social-auth
saves a user’s fullname
to our model (new_user.fullname = 'Some User'
), we’ll intercept it and save it to our database field, full_name
, instead.
through
model relationships
Django’s many-to-many relationships are a great way of handling complex object relationships simply, but they don’t afford us the ability to add custom attributes to the intermediate models
they create. By default, this simply includes an identifier and two foreign key references to join the objects together.
Using the Django ManyToManyField through
parameter, we can create this intermediate model ourselves and add any additional fields we deem necessary.
If our application, for example, not only needed users to have memberships within groups, but wanted to track when that membership started, we could use a custom intermediate model to do so.
accounts/models.py
import uuid
from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from django.utils.timezone import now
class User(AbstractBaseUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
…
class Group(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
members = models.ManyToManyField(User, through='Membership')
class Membership(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
joined = models.DateTimeField(editable=False, default=now)
In the example above, we’re still using a ManyToManyField
to handle the relationship between a user and a group, but by passing the Membership
model using the through
keyword argument, we can now add our joined
custom attribute to the model to track when the group membership was started. This through
model is a standard Django model, it just requires a primary key (we use UUIDs here), and two foreign keys to join the objects together.
Using the same three model pattern, we could create a simple subscription database for our site:
import uuid
from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from django.utils.timezone import now
class User(AbstractBaseUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
...
class Plan(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=50, unique=True, default='free')
subscribers = models.ManyToManyField(User, through='Subscription', related_name='subscriptions', related_query_name='subscriptions')
class Subscription(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(User, on_delete=models.CASCADE)
plan = models.ForeignKey(Plan, on_delete=models.CASCADE)
created = models.DateTimeField(editable=False, default=now)
updated = models.DateTimeField(auto_now=True)
cancelled = models.DateTimeField(blank=True, null=True)
Here we’re able to track when a user first subscribed, when they updated their subscription, and if we added the code paths for it, when a user canceled their subscription to our application.
Using through
models with the ManyToManyField
is a great way to add more data to our intermediate models and provide a more thorough experience for our users without much added work.
Normally in Django, when you subclass a model (this doesn’t include abstract models) into a new class, the framework will create new database tables for that class and link them (via OneToOneField
) to the parent database tables. Django calls this “multi-table inheritance” and it’s a great way to re-use existing model fields and structures and add your own data to them. “Don’t repeat yourself,” as the Django design philosophies state.
Multi-table inheritance example:
from django.db import models
class Vehicle(models.Model):
model = models.CharField(max_length=50)
manufacturer = models.CharField(max_length=80)
year = models.IntegerField(max_length=4)
class Airplane(Vehicle):
is_cargo = models.BooleanField(default=False)
is_passenger = models.BooleanField(default=True)
This example would create ...
Zac Clancy is Vice President at Global DIRT (Disaster Immediate Response Team)