Django REST Framework Tutorial : Basic CRUD actions

Welcome to my Django REST Framework tutorial, this tutorial is a reference guide for me. I can always refer to this when I want to set up a new Django REST API project.

We would be building a simple blog API.

The code to this tutorial is hosted on Github

WHAT YOU WILL LEARN FROM THIS TUTORIAL.

  1.  Basic CRUD actions.
  2. Authorization and User Authentication.
  3. Schemas and documentation.
  4. Running Tests and Deploying to  Heroku

This new post would be in series and we would begin with building the basic API part.

initial set up

Create our virtual environment and install our app dependencies.

From the terminal do the following (Mac and Linux Users)

mkdir blog_api && cd blog_api
python 3 -m venv env
source env/bin/activate
pip install -r requirements.txt

Window Users

py -m venv env
 .\env\Scripts\activate 
 pip install -r requirements.txt
django-admin startproject blog_project .
python manage.py startapp blog_api

Since we’ve added a new app we need to tell Django about it. So make sure to add blog_api to our list of INSTALLED_APPS in the settings.py file.

# blog_project/settings.py


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog_api.apps.BlogApiConfig', #new
]

For the purpose of this tutorial, we would be using the SQLite database.

cors

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP  headers to tell browsers to give a web application running at one origin, access to selected resources from a different origin. —– MDN Web Docs

Run the following on your terminal

pip install django-cors-headers

Next, update our settings.py.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    
    'corsheaders', #3rd party

    'blog_api.apps.BlogApiConfig',
]



MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',     # new
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Now run migrate on your terminal to synchronize our database with Django’s default settings and the new app.

python manage.py migrate

Designing the blog database schema

First, you need to define a Post and Category model. Add the following lines to the models.py file of the blog_api application.

from django.db import models 
from django.utils import timezone
from django.contrib.auth.models import User
from django.template.defaultfilters import slugify
from django.urls import reverse

class Category(models.Model):
   title = models.CharField(max_length=100)

   def __str__(self):        
        return self.title

class Post(models.Model):    
    STATUS_CHOICES = (        
        ('draft', 'Draft'),       
         ('published', 'Published'),   
          )    
    title = models.CharField(max_length=250)    
    slug = models.SlugField(max_length=250,null=False, unique=True)    
    author = models.ForeignKey(User,on_delete=models.CASCADE)    
    body = models.TextField()    
    publish = models.DateTimeField(default=timezone.now)    
    created = models.DateTimeField(auto_now_add=True)    
    updated = models.DateTimeField(auto_now=True)    
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    category= models.ForeignKey(Category, on_delete=models.CASCADE)    
    
    class Meta:        
        ordering = ('-publish',)   
        
    def __str__(self):        
        return self.title

    def get_absolute_url(self):
        kwargs = {
            'slug': self.slug
        }
        return reverse('post_detail', kwargs=kwargs)



    def save(self, *args, **kwargs):
        value = self.title
        self.slug = slugify(value,)
        super().save(*args, **kwargs)



 

Explaining the model fields:
• title: This is the field for the post title. This field is CharField, which translates into a VARCHAR column in the SQL database.
• slug: This is a field intended to be used in URLs (SEO-friendly URLs ). A slug is a short label that contains only letters, numbers, underscores, or hyphens. Django will prevent multiple posts from having the same slug for a given date.
• author: This field defines a many-to-one relationship, meaning that each post is written by a user, and a user can write any number of posts. For this field, we are relying on the User model of the Django authentication system. Using CASCADE, you specify that when the referenced user is deleted, the database will also delete all related blog posts.
• body: This is the body of the post.
• publish: This DateTime indicates when the post was published.
.• created: This DateTime indicates when the post was created. We are using auto_now_add here, the date will be saved automatically when creating an object.
• updated: This date-time indicates the last time the post was updated. We are using auto_now here, the date will be updated automatically when saving an object.
• status: This field shows the status of a post (published or draft)
• category: This field defines a many-to-one relationship, meaning that each post has a category, that is a Category can have multiple posts but Post will have only one category

The Meta class inside the model contains metadata. It tells Django to sort results by the publish field in descending order by default when you query the database. You specify the descending order using the negative prefix.

The __str__() method is the default human-readable representation of the object.

The save() method helps us generates slug automatically and we using Django built-in tool for this called slugify.

 

Creating and applying migrations

Run the following on your terminal

python manage.py makemigrations blog_api
python manage.py migrate

 

Create an administration site for models

First, you will need to create a user to manage the administration site. Run the following command , use username, email and password you prefer.

python manage.py createsuperuser

Adding models to the administration site

We will  customize the administration site, so let’s add the following to blog_api/admin.py as follows

from django.contrib import admin 
from .models import Category, Post



admin.site.register(Category)

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):   
     list_display = ('title', 'slug','category','author', 'publish', 'status')
     list_filter = ('status', 'created', 'publish', 'author')
     search_fields = ('title', 'body')
     prepopulated_fields = {'slug': ('title',)}
     raw_id_fields = ('author',)
     date_hierarchy = 'publish'
     ordering = ('status', 'publish')

We would use the default admin for Category but we would define  ModelAdmin object for Post.

  • The list_display attribute allows us to set the model fields we want to display on the administration page.
  • The search_fields attribute includes a search bar.
  • The ordering attribute orders the post by STATUS and PUBLISH.
  • Manually adding a slug field each time quickly becomes tedious. So we can use a prepopulated_field attribute in the admin to automate the process for us.

Now we can start up the local webserver.

python manage.py runserver

Navigate to http://127.0.0.1:8000/admin/and log in with your superuser credentials.

Click on the + Add” button next to Posts and create a new blog post.

Click on the + Add” button next to Categorys and create a new blog category.

 

Django REST Framework

Before we begin, install the following below

pip install djangorestframework

Then add it to the INSTALLED_APPS section of our settings.py file and explicitly set permissions which by default in Django REST Framework are configured toAllowAny.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework', #3rd party

    'blog_api.apps.BlogApiConfig',
]

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

Now we need to create our URLs, views, and serializers

  •  serializers.py file to transform the data into JSON
  • views.py file to apply logic to each API endpoint
  • urls.py file for the URL routes

serializers

The serializer helps to transform our data into JSON, it can also specify which fields to include or exclude.

Create a new file serializers.py in the blog_api folder

from rest_framework import serializers
from .models import Post, Category


class PostSerializer(serializers.ModelSerializer):
    class Meta:
        fields = (
            "id",
            "author",
            "title",
            "body",
            "created",
            "category",
            "status",
            "slug",
        )
        model = Post



class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        fields = (
            "id",
            "title",
        )
        model = Category

views

view is a callable that takes a request and returns a response.

Django has three types of views

  • Function based view
  • Class based view
  • Class based Generic view

REST framework provides two wrappers you can use to write API views.

  1. The @api_view decorator for working with function based views.
  2. The APIView class for working with class-based views.

 

We are going to see how to build our view with any of the types of view but for this tutorial, we finally use the class-based generic view.

Create a new file in the blog_api folder, name it function_views.py, add the code below.

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from.models import Post, Category
from.serializers import PostSerializer , CategorySerializer




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

    """
    List all posts or create a new post.
    """
    if request.method == 'GET':
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)
    
    elif request.method == 'POST':
        serializer = PostSerializer(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)


    
    
@api_view(['GET','PUT', 'DELETE'])
def post_details(request, slug):
    """
    update or delete a single post.
    """
    try:
        post = Post.objects.get(slug=slug)
    except Post.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = PostSerializer(post)
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = PostSerializer(post, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Create a new file in the blog_api folder, name it class_based_views.py, add the code below.

from.models import Post, Category
from.serializers import PostSerializer , CategorySerializer
from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView


class PostLists(APIView):
    
    """
    List all posts, or create a new post.
    """
    def get(self, request, format=None):
        posts= Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

   
    def post(self, request, format=None):
        serializer = PostSerializer(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)



class PostDetails(APIView):
    """
    Retrieve, update or delete a post instance.
    """

    def get_object(self, slug):
        try:
            return Post.objects.get(slug=slug)
        except Post.DoesNotExist:
            raise Http404

    def get(self, request, slug, format=None):
        post = self.get_object(slug)
        serializer = PostSerializer(post)
        return Response(serializer.data)

    def put(self, request, slug, format=None):
        post = self.get_object(slug)
        serializer = PostSerializer(post, 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, slug, format=None):
        post = self.get_object(slug)
        post.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

We’re going to use a class-based generic view with less code and repetition and follow Django best-practice.

Copy the code below into blog_api/views.py

from django_rest_passwordreset.signals import reset_password_token_created
from rest_framework import generics, permissions
from.models import Post, Category
from.serializers import PostSerializer , CategorySerializer



class PostListView(generics.ListCreateAPIView):
    queryset=Post.objects.all()
    serializer_class= PostSerializer
  

class CategoryListView(generics.ListCreateAPIView):
    queryset=Category.objects.all()
    serializer_class= CategorySerializer


class PostDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset=Post.objects.all()
    serializer_class= PostSerializer

class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset=Category.objects.all()
    serializer_class= CategorySerializer

URLs

Let’s start with the (blog_project/urls.py) URL routes for the actual location of the application.

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("blog_api.urls")),  # new
]

 

Next create a   urls.py file in our blog_api app.

Run the command on your terminal

touch blog_api/urls.py


from django.urls import path

from.views import CategoryDetail, CategoryListView, PostListView, PostDetail
from blog_api import function_views
from blog_api.class_based_views import PostDetails, PostLists


urlpatterns=[path('category/<int:pk>/', CategoryDetail.as_view()),
               path('category/', CategoryListView.as_view()),
               path('post/<str:slug>/', PostDetail.as_view(), name='post_detail'),
               path('post/', PostListView.as_view()),

               #FUNCTION BASED VIEWS
                path('posts/', function_views.post_lists),
                path('posts/<str:slug>/', function_views.post_details),

               #class based views
               path('blog_post/<str:slug>/', PostDetails.as_view(), name='post_detail'),
               path('blog_post/', PostLists.as_view()),
               ]

 

Our API is now complete

Browsable API

Startup the local server to interact with our API on our browser by running this on your terminal python manage.py runserver

Go to http://127.0.0.1:8000/api/v1/category   to see the CategoryListView endpoint.

Go to http://127.0.0.1:8000/api/v1/category/1/   to see the update, delete or get category with id 1

 

Go to http://127.0.0.1:8000/api/v1/post  to see the PostListView endpoint. We create a new blog post.

Go to http://127.0.0.1:8000/api/v1/post/slug/   to see the update, delete or get a post.

 

Our BlogAPI is ready.

Next post, we would be covering adding authorization (permission) and authentication to protect our API and authenticate users.

 

 

 

 

 

 

 

 

 

 

Leave a Reply

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