Creating a blog API in Django REST Framefork

by Alex
Creating a blog API in Django REST Framefork

API services allow applications to communicate with other applications using data passed in JSON format. Just create and use its data from any API client or frontend application. Django REST Framework is a toolkit for creating REST APIs using Django. In this guide, let’s look at how to use it properly. Let’s create endpoints (resource access points) for users, blog posts, comments, and categories. We’ll also look at authentication so that only the logged in user can modify application data. Here’s what you’ll learn:

  • Add new and existing Django models to the API.
  • Serialize models using built-in serializers for common API patterns.
  • Create views and URL patterns.
  • Create many-to-one and many-to-many relationships.
  • Authenticate user actions.
  • Use the Django REST Framework API created.

You can download the lesson code from the repository at https://gitlab.com/PythonRu/blogapi.

Requirements

You must have Python 3 installed on your system, preferably 3.8. You’ll also need experience with REST APIs. You should be familiar with relational databases, including primary and foreign keys, database models, migrations, and many-to-one and many-to-many relationships. Finally, experience with Python and Django will be required.

Setting up the project

To create a new API project, first create a Python virtual environment in your working directory. To do this, run the following command in the terminal:

python3 -m venv env
source env/bin/activate

On Windows it will be source envScriptsactivate. Remember to run all the commands in this manual in a virtual environment. You can verify that it is activated by writing(env) at the beginning of the prompt in the terminal. To deactivate the environment, type deactivate. After that, install Django and the REST Framework in the environment:

pip install django==3.1.7 djangorestframework==3.12.4

Create a new “blog” project and a new “api” application:

django-admin startproject blog
cd blog
django-admin startapp api

Создание нового API-проекта From the root directory “blog” (where the file “manage.py” is), synchronize the database. This will start the migrations for admin, auth, contenttypes, and sessions.

python manage.py migrate

You’ll also need the admin user to interact with the Django control panel and API. From the terminal, run the following:

python manage.py createsuperuser --email [email protected] --username admin

Set any password (it must have at least 8 characters). If you enter too simple a password, you may get an error. To configure, add rest_framework and api to the configuration file (blog/blog/settings.py):

INSTALLED_APPS = [
...
'rest_framework',
'api.apps.ApiConfig',
]

Adding apiConfig will allow you to add configuration parameters to the application. No other settings will be needed for this tutorial. Finally, start the local server with the python manage.py runserver command. Go to http://127.0.0.1:8000/admin and login to the site’s admin panel. Click on “Users” to see the user and add new ones if necessary. админ-панель сайта

Creating an API for users

Now with the user “admin” you can move on to creating the API itself. This will give read-only access to the list of users from the API endpoint list.

Serializer for User

Django’s REST Framework uses serializers to translate query sets and model instances into JSON data. The serializer also determines what data the API will return in response to a client request. Django users are created from the User model, which is defined in django.contrib.auth. To create a serializer for the model, add the following to blog/api/serializers.py (file must be created)

from rest_framework import serializers
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username']

For example, import the User model from Django along with a set of serializers from Django’s REST Framework. Now create a UserSerializer class, which should inherit from the ModelSerializer class. Define a model to be associated with the serializer (model = User). The fields array defines which model fields should be included. For example, you can add first_name and last_name fields. The ModelSerializer class generates serializer fields that are based on the corresponding model properties. This means that you don’t have to manually specify all the attributes for the serializer field, because they are pulled directly from the model. This serializer also creates simple create() and update() methods. You can rewrite them if necessary. You can read more about how the ModelSerializer works on the official website.

Views for User

There are several ways to create views in Django’s REST Framework. To get code reusability and avoid repetition, use class views. The REST Framework provides several generalized views based on the APIView class. They represent the most common patterns. For example, ListAPIView is used for read-only endpoints. It provides a get handler method. ListCreateAPIView is used for endpoints with read-write permission, and provides get and post handlers. To create a read-only endpoint that returns a list of users, add the following to blog/api/views.py:

from rest_framework import generics
from . import serializers
from django.contrib.auth.models import User
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = serializers.UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = serializers.UserSerializer

The generics collection of views is imported here first, along with the User and UserSerialized model from the previous step. The UserList view provides read-only access (via get) to the list of users, and the UserDetails view provides access to a single user. The names of the views should be in the following format: {ModelName}List and {ModelName}Details for a collection of objects and a single object, respectively. For each view, the variable queryset contains the collection of model instances that User.objects.all() returns. The value of serializer_class must be UserSerializer, which serializes the User model data. The paths to the endpoints will be configured in the next step.

URL patterns

With the model, serializer, and set of views for the User, the final step is to create endpoints (called URL patterns in Django) for each view. First add the following to blog/api/urls.py (you need to create this file too):

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from . import views
urlpatterns = [
path('users/', views.UserList.as_view()),
path('users//', views.UserDetail.as_view()),
]
urlpatterns = format_suffix_patterns(urlpatterns)

This is where the path function is imported and also a collection of api application views. The path function creates the element that Django uses to display the application page. To do this, Django first searches for the desired element with the appropriate URL (e.g., users/) for the user requested. It then imports and calls the corresponding view (i.e., UserList) The sequence points to an integer value, which is the primary key(pk). Django captures this part of the URL and sends it to the view as a keyword argument. In this case, the primary key for User is the id field, so http://127.0.0.1:8000/users/1 will return id with a value of 1. Before you can interact with these URL patterns (and those that will be created later) they need to be added to the project. Add the following to blog/blog/urls.py:

from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('api.urls')),
]

To make sure all the elements work correctly, go to http://127.0.0.1:8000/users to see the list of app users. Django REST framework User List This tutorial uses a graphical representation of the API from the Django REST Framework to demonstrate the endpoints. The interface provides authentication elements and forms that mimic the frontend client. You can also use cURL or httpie to test the API. Note that the admin user value is 1. You can go to it by opening http://127.0.0.1:8000/users/1 for that. Django REST framework User Detail Finally, the Django model class is serialized using UserSerializaer. It provides data to the UserList and UserDetail views, which can be accessed using the users/ and users/ patterns.

Creating the API for Post

After some basic configuration, you can start building the full-fledged blog API with endpoints for posts, comments and categories. Let’s start with the API for Post.

Post model

In blog/api/models.py create a Post model that inherits from the Model class of Django and define its fields:

from django.db import models
class Post(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
body = models.TextField(blank=True, default='')
owner = models.ForeignKey('auth.User', related_name='posts', on_delete=models.CASCADE)
class Meta:
ordering = ['created']

The field types correspond to those in relational databases. You can check the Models page on the official website of the framework. Note that the ForeignKey type creates a many-to-one relationship between the current model and the model specified in the first argument(auth.User – that is, the User model you are working with). In this case, the user can have many articles, but the post can have only one owner. The owner field can be used in the frontend application to get the user and display his name as the author of the post. The related_name argument allows you to specify another access name for the current model(posts) instead of the standardone (post_set). The list of posts will be added to the User serializer in the next step to complete the many-to-one relationship. Each time you update the model, run the following commands to update the database:

python manage.py makemigrations api
python manage.py migrate

Since we are working with Django models such as User, the posts can be modified from the Django administrative panel by registering it in blog/api/admin.py:

from django.contrib import admin
from .models import Post
admin.site.register(Post)

You can also create them later via the graphical API view. Go to http://127.0.0.1:8000/admin, click on Posts and add new posts. You’ll notice that the title and body fields in the form correspond to the CharField and TextField types from the Post model. You can also select owner among existing users. There is no need to select an owner when creating a post in the API. The owner will be set automatically based on the data of the logged in user. We will set it up in the next step.

Post serializer

To add Post model to API you need to repeat the steps of adding User model. First we need to serialize the data of the Post model. In blog/api/serializers.py add the following:

from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Post
class PostSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Post
fields = ['id', 'title', 'body', 'owner']
class UserSerializer(serializers.ModelSerializer):
posts = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'posts']

Import the Post model from the api application and create a PostSerializer that will inherit from the ModelSerializer class. Define the model and fields to be used by the serializer. ReadOnlyField is a class that returns unchanged data. In this case it is used to return the username field instead of the standard id. Next, add the posts field to the UserSerializer. The many-to-one relationship between posts and users is defined by the Post model in the last step. The name of the field(posts) should be equal to the related_field argument of post.owner. Replace posts with post_set (the default) if you did not specify a related_field value in the last step. PrimaryKeyRelatedField represents the list of publications in this many-to-one relationship(many=True indicates that there may be more than one post). If you do not set read_only=True the posts field will have default write permissions. This means that it will be possible to manually set the list of articles belonging to the user when they are created. This is hardly the desired behavior. Go to http://127.0.0.1:8000/users to see each user’s posts field. Note that the list of posts is essentially a list of id’s. Instead, you can return a list of URLs using HyperLinkModelSerializer.

Post Views

The next step is to create a set of views for the Post API. Add the following to blog/api/views.py:

...
from .models import post
...
class PostList(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = serializers.PostSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Post.objects.all()
serializer_class = serializers.PostSerializer

ListCreateAPIView and RetrieveUpdateDestroyAPIView provide the most common API method handlers: get and post for a list(ListCreateAPIView) and get, update and delete for a single entity(RetrieveUpdateDestroyAPIView). You also need to overwrite the default perform_create function to set the owner field of the current user (value self.request.user).

URL patterns Post

To finish up with the endpoints for the Post API, create Post URL patterns. Add the following to the urlpatterns array in blog/api/urls.py:

urlpatterns = [
...
path('posts/', views.PostList.as_view()),
path('posts//', views.PostDetail.as_view()),
]

Combining views with these URL-patterns creates endpoints:

  • get posts/,
  • getposts/,
  • get posts//,
  • put posts//
  • and delete posts//.

To test them, go to http://127.0.0.1:8000/posts and create publications. I took some articles from Medium. Go to one post (e.g. http://127.0.0.1:8000/posts/1 and press DELETE. To change the title of the post, update the “title” field and click PUT. Django REST framework Post Detail Then go to http://127.0.0.1:8000/posts to see a list of existing posts or to create a new one. Make sure you are logged in, because when you create a post, its author is created based on the current user. Django REST framework Post List

Setting Permissions

For convenience, let’s add a “Log in” button to the API graphical view with the following code in blog/urls.py:

urlpatterns = [
...
path('api-auth/', include('rest_framework.urls')),
]

You can now log in under different accounts to check that permissions work and change posts through the interface. Now you can create a post while being logged in, but you don’t need that to delete or modify data – even if you don’t own the post. Try logging in with a different account, and you may be able to delete posts belonging to admin. To authenticate the user and make sure only the owner of the post can update and delete it, you need to add permissions. Start with this code in blog/api/permisisions.api (file must be created):

from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user

The IsOwnerOrReadOnly permission checks if the user is the owner of this object. So only under this condition will it be possible to update or delete a post. Non-owners will be able to receive the post because this is a read-only action. There is also a built-in IsAuthenticatedOrReadOnly permission. With it, any authenticated user can make any request, and the rest can read-only. Add these permissions to the Post views:

from rest_framework import generics, permissions
...
from .serializers import PostSerializer
from .permissions import IsOwnerOrReadOnly
...
class PostList(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
permissions_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class PostDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
permissions_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]

The PostList view needs the IsAuthenticatedOrReadOnly permission because the user must be authenticated to create the post, but any user can view the list. PostDetails requires both permissions because only the logged-in user and the post owner must update or delete the post. No permissions are needed for postDetails. Go back to http://127.0.0.1:8000/posts. Go to the admin account and others to check what actions are available to authenticated and anonymous users. When you are unlogged, you should not be able to create, delete or update posts. When authenticated, you should not have the right to delete or edit other people’s posts. Django REST framework Log Out

Creating an API for Comments

You now have a basic API for posts. You can add comments to the system. A comment is text that a user adds in response to another user’s post. You can have multiple comments to one post, and a post can have multiple comments from different users. This means that you need to set up two pairs of many-to-one relationships: between comments and users, and between comments and posts.

Comment model

First, create a model in blog/api/models.py:

...
class Comment(models.Model):
created = models.DateTimeField(auto_now_add=True)
body = models.TextField(blank=False)
owner = models.ForeignKey('auth.User', related_name='comments', on_delete=models.CASCADE)
post = models.ForeignKey('Post', related_name='comments', on_delete=models.CASCADE)
class Meta:
ordering = ['created']

The Comment model is similar to Post and has a many-to-one relationship with users via the owner field. Comment has a many-to-one relationship with one post via the post field. Run the database migrations:

python manage.py makemigrations api
python manage.py migrate

Comment Serializer

To create the Comment API, you need to add the Comment model to the PostSerializer and UserSerializer to make sure that related comments are sent along with user and post data. Update the code in blog/api/serializers.py:

...
from .models import Post, Comment
class PostSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
comments = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'body', 'owner', 'comments']
class UserSerializer(serializers.ModelSerializer):
posts = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
comments = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'posts', 'comments']

The process is similar to adding posts to UserSerializer. This sets up the “many-to-many” part of the many-to-one relationship between comments and the user, as well as between comments and the post. The comment list must be read-only(read_only=True) Now let’s add CommentSerializer to the same file:

...
class CommentSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Comment
fields = ['id', 'body', 'owner', 'post']

Note that the post field does not change. Once the post field is added to the fields array, it will be serialized by default (according to ModelSerializer). This is equivalent to post=serializers.PrimaryKeyRelatedField(queryset=Post.objects.all()). This means that the post field has a default write right: when you create a comment, you configure which post it belongs to.

Comment views

Finally, let’s create representations and patterns for comments. The process is similar to the one we went through when setting up the Post API. Add this code to blog/api/views.py:

...
from .models import Post, Comment
...
class CommentList(generics.ListCreateAPIView):
queryset = Comment.objects.all()
serializer_class = serializers.CommentSerializer
permissions_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class CommentDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Comment.objects.all()
serializer_class = serializers.CommentSerializer
permissions_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]

The views are similar to PostList and PostDetails.

Comment URL-Patterns

To finish the comments API, define URL-patterns in blog/api/urls.py:

urlpatterns = [
...
path('comments/', views.CommentList.as_view()),
path('comments//', views.CommentDetail.as_view()),
]

Now at http://127.0.0.1:8000/comments you can see a list of existing comments and create new ones. Django REST framework Comment List Also note that you have to select a post from the list of existing posts when you create one.

Creating the Category API

The final element of the blog is the category system. A post can belong to one or more categories. Also, one category can belong to multiple posts, meaning it is a many-to-many relationship.

Category Model

Create a Category model in blog/api/models.py:

urlpatterns = [
...
path('comments/', views.CommentList.as_view()),
path('comments//', views.CommentDetail.as_view()),
]

Here, the ManyToManyField class creates a many-to-many relationship between the current model and the model from the first argument. As with the ForeignKey class, the relationship completes the serializer. Note that verbose_name_plural defines how to correctly spell a model name in the plural. This is needed, for example, for the administration panel. So, you can specify that the plural is correct to write categories, not categories. Run the database migrations:

python manage.py makemigrations api
python manage.py migrate

Category serializer

The creation process is similar to that described in the previous steps. First, create a serializer for Category by adding code to blog/api/serializers.py:

...
from .models import Post, Comment, Category
...
class CategorySerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
posts = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Category
fields = ['id', 'name', 'owner', 'posts']
class PostSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
comments = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'body', 'owner', 'comments', 'categories']
class UserSerializer(serializers.ModelSerializer):
posts = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
comments = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
categories = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'posts', 'comments', 'categories']

Remember to add the name of the categories field to the list of PostSerializer and UserSerializer fields. Note that you can mark UserSerializer.categories as read_only=True. This field represents the list of all created categories. On the other hand, the PostSerializer.categories field will have a default write right. The same as specifying categories = serializers.PrimaryKeyRelatedField(many=True, queryset=Category.objects.all()). This will allow the user to select one or more categories for the post.

Views for the category

Next, create views in blog/api/views.api:

...
from .models import Post, Comment, Category
...
class CategoryList(generics.ListCreateAPIView):
queryset = Category.objects.all()
serializer_class = serializers.CategorySerializer
permissions_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Category.objects.all()
serializer_class = serializers.PostSerializer
permissions_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]

Similar to the past ones.

Category URL patterns

Finally, add this code to blog/api/urls.py:

...
from .models import Post, Comment, Category
urlpatterns = [
...
path('categories/', views.CategoryList.as_view()),
path('categories//', views.CategoryDetail.as_view()),
]

Now go to http://127.0.0.1:8000/categories and create a couple of categories. And at http://127.0.0.1:8000/posts, create a post and select categories for it. Django REST framework Categories

Conclusions

You now have a blog API with authentication and many common API patterns. There are endpoints for getting, creating, updating, and deleting posts, categories, and comments. There are also many-to-one and many-to-many relationships set up between these resources. To extend the capabilities of your API, go to the official Django REST Framework documentation. And a link to the code for this tutorial can be found at the beginning of this article.

Related Posts

LEAVE A COMMENT