Django REST framework is a powerful and flexible toolkit for building Web APIs. You can easily build a REST API using DRF and consume the endpoints from a React, Angular, or other Frontend application. DRF provides a lot of features out of the box to make the development process easier and faster. In this tutorial, we will build a blog API with the following features:
- Custom user model where email is the unique identifier instead of email.
- JWT-based authentication.
- Ability to create, retrieve, update, and delete posts.
- Like/Dislike feature for posts.
- Ability to comment on posts.
Note:- If you encounter any issues throughout the tutorial, you can check out the code in the GitHub repository.
Table of Contents
- Prerequisite
- Project Configuration
- Custom User Model in Django for Email-Based Auth
- User Profile
- JWT Authentication
- User Registration and Login Endpoints
- The Blog API
Prerequisite
This guide assumes that you have intermediate-level knowledge of Django and Django Rest Framework.
Project Configuration
First, create a virtual environment and activate it:
python3 -m venv .venv
source .venv/bin/activate
Next, install Django and create a new Django project:
pip install django==4.1.2
django-admin startproject config .
Custom User Model in Django for Email-Based Auth
One of the things that make Django really great is its built-in User
model that comes with username-based authentication. In no time, you can have authentication that works out of the box. However, not all projects need username and password pair for authentication, or in some cases, you might want to include additional fields in the built-in User
model. Either way, you are in luck because Django also provides room for customization. In this tutorial, we will tweak the default User
model to use email as the unique primary identifier of users instead of a username.
Other use cases for a custom user model include:
- Including other unique fields for authentication like a phone number.
- To stack all user information, be it auth-related or non-auth fields all in the
User
model.
Alright, let’s now create an app named users
that will contain the logic we just talked about.
py manage.py startapp users
Add it to the installed apps list in the settings:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Local apps
'users',
]
There are two ways you can customize the User
model. Either by extending from AbstractUser
or AbstractBaseUser
What’s the difference and when should you use one over the other? Glad you asked.
-
AbstractUser
: Are you satisfied with the existing fields in the built-inUser
model but do you want to use email as the primary unique identifier of your users or perhaps remove theusername
field? or do you want to add fields to the existingUser
? If yes,AbstractUser
is the right option for you. -
AbstractBaseUser
: This class contains authentication functionality but no fields, so you have to add all the necessary fields when you extend from it. You probably want to use this to have more flexibility on how you want to handle users.
Note: If you’re starting a new project, it’s highly recommended to set up a custom user model, even if the default
User
model is sufficient for you. This model behaves identically to the default user model, but you’ll be able to customize it in the future if the need arises:
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
Custom Model Manager
A Manager
is a class that provides an interface through which database query operations are provided to Django models. You can have more than one manager for your model.
Consider this model:
from django.db import models
class Car(models.Model):
pass
- To get all instances of
Car
, you will useCar.objects.all()
objects
is the default name that Django managers use. To change this name, you can do the following:
from django.db import models
class Car(models.Model):
cars = models.Manager();
Now, to get all instances of car, you should use Car.cars.all()
For our custom user model, we need to define a custom manager class because we are going to modify the initial Queryset
that the default Manager
class returns. We do this by extending from BaseUserManager and providing two additional methods create_user
and create_superuser
Create a file named managers.py
inside the users
app and put the following:
# users/managers.py
from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import gettext_lazy as _
class CustomUserManager(BaseUserManager):
"""
Custom user model manager where email is the unique identifier
for authentication instead of usernames.
"""
def create_user(self, email, password, **extra_fields):
if not email:
raise ValueError(_("Users must have an email address"))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError(_("Superuser must have is_staff=True."))
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True."))
return self.create_user(email, password, **extra_fields)
Then, create the custom user model as follows:
# users/models.py
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _
from .managers import CustomUserManager
class CustomUser(AbstractUser):
email = models.EmailField(_("email address"), unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
objects = CustomUserManager()
def __str__(self):
return self.email
-
USERNAME_FIELD
specifies the name of the field on the user model that is used as the unique identifier. In our case it’s email. -
REQUIRED_FIELDS
A list of the field names that will be prompted when creating a superuser via thecreatesuperuser
management command. This doesn’t have any effect in other parts of Django like when creating a user in the admin panel.
Next, we need to tell Django about the new model that should be used to represent a User. This is done as follows:
# config/settings.py
AUTH_USER_MODEL = 'users.CustomUser'
Finally, create and apply migrations:
py manage.py makemigrations
py manage.py migrate
Forms
You need to extend Django's built-in UserCreationForm
and UserChangeForm
forms so that they can use the new user model that we are working with.
Create a file named forms.py
inside the users
app and add the following:
# users/forms.py
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ("email",)
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ("email",)
Admin
Tell the admin panel to use these forms by extending from UserAdmin
in users/admin.py
# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserChangeForm, CustomUserCreationForm
from .models import CustomUser
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = (
"username",
"email",
"is_active",
"is_staff",
"is_superuser",
"last_login",
)
list_filter = ("is_active", "is_staff", "is_superuser")
fieldsets = (
(None, {"fields": ("username", "email", "password")}),
(
"Permissions",
{
"fields": (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Dates", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"email",
"password1",
"password2",
"is_staff",
"is_active",
),
},
),
)
search_fields = ("email",)
ordering = ("email",)
-
add_form
andform
specify the forms to add and change user instances. -
fieldsets
specify the fields to be used in editing users andadd_fieldsets
specify fields to be used when creating a user.
And that’s all you need. You can now go to the admin panel and add/edit users.
User Profile
Let’s now create a user profile. This includes non-auth-related fields for the user. For now, this model contains an avatar and bio. You must already be familiar with modeling a profile for the user. Basically, this is done by using a one-to-one relationship between the User
and Profile
model. In a one-to-one relationship, one record in a table is associated with one and only one record in another table using a foreign key. For example - a user model instance is associated with one and only one profile instance.
Head over to users/models.py
and add the following:
# users/models.py
import os
from django.conf import settings
from django.db import models
from django.template.defaultfilters import slugify
def get_image_filename(instance, filename):
name = instance.product.name
slug = slugify(name)
return f"products/{slug}-{filename}"
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to=get_image_filename, blank=True)
bio = models.CharField(max_length=200, blank=True)
def __str__(self):
return self.user.email
@property
def filename(self):
return os.path.basename(self.image.name)
Whenever you are using an ImageField
in Django, you need to install Pillow
which is one of the most common image-processing libraries in Python. Let’s install it:
pip install pillow==9.3.0
Next, let’s register the profile model in line with the custom user model as follows:
# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserChangeForm, CustomUserCreationForm
from .models import CustomUser, Profile
class ProfileInline(admin.StackedInline):
model = Profile
can_delete = False
verbose_name_plural = "Profile"
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = (
"username",
"email",
"is_active",
"is_staff",
"is_superuser",
"last_login",
)
list_filter = ("is_active", "is_staff", "is_superuser")
fieldsets = (
(None, {"fields": ("username", "email", "password")}),
(
"Permissions",
{
"fields": (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Dates", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"email",
"password1",
"password2",
"is_staff",
"is_active",
),
},
),
)
search_fields = ("email",)
ordering = ("email",)
inlines = (ProfileInline,)
admin.site.register(Profile)
Since we are working with user-uploaded images, we need to set MEDIA_URL and MEDIA_ROOT in the settings:
# config/settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
Next, configure the project's urls.py
to serve user-uploaded media files during development.
# config/urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Before testing this out, let’s create a signal to automatically create a user profile when a user is created. Create a file named signals.py
and add the following:
# users/signals.py
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Profile
User = get_user_model()
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.profile.save()
Finally, connect the receivers in the ready()
method of the app's configuration by importing the signals module. Head over to users/apps.py
and add the following:
# users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"
def ready(self):
import users.signals
Test it out:
py manage.py makemigrations
py manage.py migrate
py manage.py runserver
JWT Authentication
The default authentication system that Django provides is session-based. Sessions in Django are implemented using the django.contrib.sessions.middleware.SessionMiddleware
middleware. This session-based auth works well with the traditional HTML request-response cycle. However, if you have a client that expects the server to return a JSON response instead of an HTML, you are going to have to use token authentication or JWT authentication and let the client decide what to do with the JSON response. In this tutorial, we will implement JWT authentication using Django Rest Framework.
What is JWT?
JSON Web Token (JWT) is a cryptographically signed URL-safe token for securely transmitting information between parties as a JSON object.
In JWT-based auth, the following happens:
- The client sends the username and password to the server.
- The server validates user credentials against the database.
-
The server generates and sends to the client a secure JWT token that is signed using a secret key. This token is of the format:
header.payload.signature
Decoding tokens in the above format will give information about the user like ID, username, etc.
The client then includes this token in the HTTP header for subsequent requests.
The server verifies the token using the secret key without hitting the database. If the token has been tampered with, the client’s request will be rejected.
This token (also called access token), although customizable, is usually short-lived. Along with the access token, the server also generates and sends to the client a refresh token. A refresh token has a longer life and you can exchange it for an access token.
JWT thus is scalable and fast because of fewer database hits.
Alright, let’s first install Django Rest Framework:
pip install djangorestframework==3.14.0
Add it to installed apps settings:
# config/settings.py
INSTALLED_APPS = [
# ...
"rest_framework",
]
To implement JWT auth in our project, we are going to make use of djangorestframework_simplejwt. Install it:
pip install djangorestframework-simplejwt==5.2.2
Then, tell DRF the authentication backend we want to use:
# config/settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}
Here are a couple of setting variables for simple JWT that can be customized in the settings:
# config/settings.py
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=14),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": False,
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"VERIFYING_KEY": None,
"AUDIENCE": None,
"ISSUER": None,
"JWK_URL": None,
"LEEWAY": 0,
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
- If
ROTATE_REFRESH_TOKENS
is set toTrue
, a new refresh token will be returned along with the access token. And ifBLACKLIST_AFTER_ROTATION
is set toTrue
, refresh token submitted to the refresh view will be added to the blacklist. You need to add'rest_framework_simplejwt.token_blacklist'
to the list of installed apps for theBLACKLIST_AFTER_ROTATION
setting to work. so let’s do that:
# config/settings.py
# Third-party apps
INSTALLED_APPS = [
# ...
"rest_framework_simplejwt.token_blacklist",
]
Finally, run the following command to apply the app’s migrations:
py manage.py migrate
Now, we need to create access and refresh tokens when the user registers/login. In the next section, we will add serializer and views to accomplish this task.
User Registration and Login Endpoints
Create a file named serializers.py
inside the users
app and add the following:
# users/serializers.py
from django.contrib.auth import authenticate
from rest_framework import serializers
from .models import CustomUser, Profile
class CustomUserSerializer(serializers.ModelSerializer):
"""
Serializer class to serialize CustomUser model.
"""
class Meta:
model = CustomUser
fields = ("id", "username", "email")
class UserRegisterationSerializer(serializers.ModelSerializer):
"""
Serializer class to serialize registration requests and create a new user.
"""
class Meta:
model = CustomUser
fields = ("id", "username", "email", "password")
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
return CustomUser.objects.create_user(**validated_data)
class UserLoginSerializer(serializers.Serializer):
"""
Serializer class to authenticate users with email and password.
"""
email = serializers.CharField()
password = serializers.CharField(write_only=True)
def validate(self, data):
user = authenticate(**data)
if user and user.is_active:
return user
raise serializers.ValidationError("Incorrect Credentials")
class ProfileSerializer(CustomUserSerializer):
"""
Serializer class to serialize the user Profile model
"""
class Meta:
model = Profile
fields = ("bio",)
class ProfileAvatarSerializer(serializers.ModelSerializer):
"""
Serializer class to serialize the avatar
"""
class Meta:
model = Profile
fields = ("avatar",)
- Note that we have also created a serializer class for the profile.
Then in the views.py
# users/views.py
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from . import serializers
from .models import Profile
User = get_user_model()
class UserRegisterationAPIView(GenericAPIView):
"""
An endpoint for the client to create a new User.
"""
permission_classes = (AllowAny,)
serializer_class = serializers.UserRegisterationSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
token = RefreshToken.for_user(user)
data = serializer.data
data["tokens"] = {"refresh": str(token), "access": str(token.access_token)}
return Response(data, status=status.HTTP_201_CREATED)
class UserLoginAPIView(GenericAPIView):
"""
An endpoint to authenticate existing users using their email and password.
"""
permission_classes = (AllowAny,)
serializer_class = serializers.UserLoginSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data
serializer = serializers.CustomUserSerializer(user)
token = RefreshToken.for_user(user)
data = serializer.data
data["tokens"] = {"refresh": str(token), "access": str(token.access_token)}
return Response(data, status=status.HTTP_200_OK)
class UserLogoutAPIView(GenericAPIView):
"""
An endpoint to logout users.
"""
permission_classes = (IsAuthenticated,)
def post(self, request, *args, **kwargs):
try:
refresh_token = request.data["refresh"]
token = RefreshToken(refresh_token)
token.blacklist()
return Response(status=status.HTTP_205_RESET_CONTENT)
except Exception as e:
return Response(status=status.HTTP_400_BAD_REQUEST)
class UserAPIView(RetrieveUpdateAPIView):
"""
Get, Update user information
"""
permission_classes = (IsAuthenticated,)
serializer_class = serializers.CustomUserSerializer
def get_object(self):
return self.request.user
class UserProfileAPIView(RetrieveUpdateAPIView):
"""
Get, Update user profile
"""
queryset = Profile.objects.all()
serializer_class = serializers.ProfileSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user.profile
class UserAvatarAPIView(RetrieveUpdateAPIView):
"""
Get, Update user avatar
"""
queryset = Profile.objects.all()
serializer_class = serializers.ProfileAvatarSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user.profile
- The above views are self-explanatory. Basically, the views for user authentication use the
RefreshToken
class of simple JWT to generate and send to the client refresh and access tokens. In addition, the logout view blacklists the refresh token. The other views are used to get or update a user and his/her profile.
Now, let’s hook our views in the URLs.
Head over to config/urls.py
and add the users
app URLs:
# config/urls.py
from django.urls import include, path
urlpatterns = [
# ...
path("", include("users.urls", namespace="users")),
]
Inside the users
app, create a file named urls.py
and add the endpoints as follows:
# users/urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from users import views
app_name = "users"
urlpatterns = [
path("register/", views.UserRegisterationAPIView.as_view(), name="create-user"),
path("login/", views.UserLoginAPIView.as_view(), name="login-user"),
path("token/refresh/", TokenRefreshView.as_view(), name="token-refresh"),
path("logout/", views.UserLogoutAPIView.as_view(), name="logout-user"),
path("", views.UserAPIView.as_view(), name="user-info"),
path("profile/", views.UserProfileAPIView.as_view(), name="user-profile"),
path("profile/avatar/", views.UserAvatarAPIView.as_view(), name="user-avatar"),
]
- Note that the
token/refresh
endpoint will be used to get a new access and refresh token.
The Blog API
First, create an app named posts
py manage.py startapp posts
and add it to the list of installed apps in the settings:
# config/settings.py
INSTALLED_APPS = [
# ...
"posts",
]
Let’s wire up the models. We are going to have 3 models. The Post
, Category
, and Comment
A post can have many categories and comments, thus we are going to use a ManyToMany
field:
# posts/models.py
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
class Category(models.Model):
name = models.CharField(_("Category name"), max_length=100)
class Meta:
verbose_name = _("Category")
verbose_name_plural = _("Categories")
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(_("Post title"), max_length=250)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name="posts",
null=True,
on_delete=models.SET_NULL,
)
categories = models.ManyToManyField(Category, related_name="posts_list", blank=True)
body = models.TextField(_("Post body"))
likes = models.ManyToManyField(
settings.AUTH_USER_MODEL, related_name="post_likes", blank=True
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("-created_at",)
def __str__(self):
return f"{self.title} by {self.author.username}"
class Comment(models.Model):
post = models.ForeignKey(Post, related_name="comments", on_delete=models.CASCADE)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name="post_comments",
null=True,
on_delete=models.SET_NULL,
)
body = models.TextField(_("Comment body"))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("-created_at",)
def __str__(self):
return f"{self.body[:20]} by {self.author.username}"
Now, register these models in the admin:
# posts/admin.py
from django.contrib import admin
from .models import Category, Comment, Post
admin.site.register(Category)
admin.site.register(Post)
admin.site.register(Comment)
Create and run migrations:
py manage.py makemigrations
py manage.py migrate
Great! let’s now set up the serializer classes and views.
Create serializers.py
inside the posts
app and add the following:
# posts/serializers.py
from rest_framework import serializers
from .models import Category, Comment, Post
class CategoryReadSerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = "__all__"
class PostReadSerializer(serializers.ModelSerializer):
author = serializers.CharField(source="author.username", read_only=True)
categories = serializers.SerializerMethodField(read_only=True)
likes = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Post
fields = "__all__"
def get_categories(self, obj):
categories = list(
cat.name for cat in obj.categories.get_queryset().only("name")
)
return categories
def get_likes(self, obj):
likes = list(
like.username for like in obj.likes.get_queryset().only("username")
)
return likes
class PostWriteSerializer(serializers.ModelSerializer):
author = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Post
fields = "__all__"
class CommentReadSerializer(serializers.ModelSerializer):
author = serializers.CharField(source="author.username", read_only=True)
class Meta:
model = Comment
fields = "__all__"
class CommentWriteSerializer(serializers.ModelSerializer):
author = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Comment
fields = "__all__"
- Separating serializers for read and write is something that can be really helpful because you may sometimes want to include more details in the response for read (list and retrieve) but limit the number of fields when adding an entry to the database. This makes your serializer classes less complicated.
- Also, note the use of
serializers.CurrentUserDefault
. This is really hand-in to automatically set the authenticated user as the author or owner of something.
Next, to write our views, we are going to use ViewSets
. If you are new to ViewSets, here is a quick overview.
ViewSet
is a type of class-based view that combines the logic for a set of related views into a single class. The 2 most common types of ViewSets that you are most likely to use are Modelviewset
and ReadOnlyModelViewSet
. One of the main advantages of ViewSets is to have URL endpoints automatically defined for you through Routers
ViewSets have the highest level of abstraction and you can use them to avoid writing all the code for basic and repetitive stuff. They are a huge time-saver!
That being said, add the following code in the views and read the comments for further explanation.
# posts/views.py
from django.shortcuts import get_object_or_404
from rest_framework import permissions, status, viewsets
from rest_framework.response import Response
from rest_framework.views import APIView
from posts.models import Category, Comment, Post
from posts.serializers import (
CategoryReadSerializer,
CommentReadSerializer,
CommentWriteSerializer,
PostReadSerializer,
PostWriteSerializer,
)
from .permissions import IsAuthorOrReadOnly
# Category is going to be read-only, so we use ReadOnlyModelViewSet
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
"""
List and Retrieve post categories
"""
queryset = Category.objects.all()
serializer_class = CategoryReadSerializer
permission_classes = (permissions.AllowAny,)
class PostViewSet(viewsets.ModelViewSet):
"""
CRUD posts
"""
queryset = Post.objects.all()
# In order to use different serializers for different
# actions, you can override the
# get_serializer_class(self) method
def get_serializer_class(self):
if self.action in ("create", "update", "partial_update", "destroy"):
return PostWriteSerializer
return PostReadSerializer
# get_permissions(self) method helps you separate
# permissions for different actions inside the same view.
def get_permissions(self):
if self.action in ("create",):
self.permission_classes = (permissions.IsAuthenticated,)
elif self.action in ("update", "partial_update", "destroy"):
self.permission_classes = (IsAuthorOrReadOnly,)
else:
self.permission_classes = (permissions.AllowAny,)
return super().get_permissions()
class CommentViewSet(viewsets.ModelViewSet):
"""
CRUD comments for a particular post
"""
queryset = Comment.objects.all()
def get_queryset(self):
res = super().get_queryset()
post_id = self.kwargs.get("post_id")
return res.filter(post__id=post_id)
def get_serializer_class(self):
if self.action in ("create", "update", "partial_update", "destroy"):
return CommentWriteSerializer
return CommentReadSerializer
def get_permissions(self):
if self.action in ("create",):
self.permission_classes = (permissions.IsAuthenticated,)
elif self.action in ("update", "partial_update", "destroy"):
self.permission_classes = (IsAuthorOrReadOnly,)
else:
self.permission_classes = (permissions.AllowAny,)
return super().get_permissions()
# Here, we are using the normal APIView class
class LikePostAPIView(APIView):
"""
Like, Dislike a post
"""
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, pk):
user = request.user
post = get_object_or_404(Post, pk=pk)
if user in post.likes.all():
post.likes.remove(user)
else:
post.likes.add(user)
return Response(status=status.HTTP_200_OK)
We haven’t created custom permission to limit edit and delete actions to the owner of a post, so let’s go ahead and do that. Create a file named permissions.py
inside the posts
app and add the following:
# posts/permissions.py
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""
Check if authenticated user is author of the post.
"""
def has_permission(self, request, view):
return request.user.is_authenticated is True
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.user
Finally, let’s configure the URLs:
# config/urls.py
from django.urls import include, path
urlpatterns = [
# ...
path("post/", include("posts.urls", namespace="posts")),
]
# posts/urls.py
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import CategoryViewSet, CommentViewSet, LikePostAPIView, PostViewSet
app_name = "posts"
router = DefaultRouter()
router.register(r"categories", CategoryViewSet)
router.register(r"^(?P<post_id>\d+)/comment", CommentViewSet)
router.register(r"", PostViewSet)
urlpatterns = [
path("", include(router.urls)),
path("like/<int:pk>/", LikePostAPIView.as_view(), name="like-post"),
]
Great! that’s all. You can use Postman or the built-in browsable API to test the endpoints. Note that, if you use the browsable API, you need to add session authentication to the DEFAULT_AUTHENTICATION_CLASSES
because the browsable API uses session authentication for the login form. To do so, head over to the settings and update the DEFAULT_AUTHENTICATION_CLASSES
setting:
# config/settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
}
Then, in the project’s urls.py
file add the API URL:
# config/urls.py
from django.urls import include, path
urlpatterns = [
path("api-auth/", include("rest_framework.urls")),
]
P.S. don’t forget to configure CORS to allow in-browser requests from other origins like your React app for example.
Happy coding! 🖤