Kate Bartolo I am a developer and technical writer with a love of language and all things open source. I write about web development, game development, and documentation at kabartolo.com.

Use Django REST Framework to build a blog

14 min read 4169

Django REST Framework Logo Over a Desk Background

API services allow applications to connect to other applications using JSON-encoded data. Create an API once, and consume it from any API client or front-end application.

Django REST Framework is a toolkit for building REST APIs with Django. In this tutorial, you’ll use Django REST Framework to build a blog API. This API will have endpoints for users, blog posts, comments, and categories.

You’ll also learn how to authenticate user actions to ensure only authenticated users can modify your app’s data.

This API project demonstrates the following skills:

  • Adding new and existing Django models to an API
  • Serializing these models using built-in serializers for common API patterns
  • Creating views and URL patterns
  • Defining many-to-one and many-to-many relationships
  • Authenticating user actions
  • Using Django REST Framework’s browsable API

Prerequisites for using Django REST Framework

You should have Python 3 installed on your system, as well as experience interacting with REST APIs. You should also be familiar with relational databases, including primary and foreign keys, database models, migrations, and many-to-one and many-to-many relationships.

You’ll also need some experience with Python and Django.

Setting up the Python environment

To create a new API project, first set up a Python environment in your working directory. Run the following in your terminal:

python3 -m venv env
source env/bin/activate

On Windows, run source env\Scripts\activate instead.

Be sure to run all commands in this tutorial from this virtual environment (make sure you see (env) at the beginning of the input line in your terminal).

To deactivate this environment, run deactivate.

Next, install Django and Django REST framework into the virtual environment:

pip install django
pip install djangorestframework

Then create a new project called blog and an app called api:

We made a custom demo for .
No really. Click here to check it out.

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

From the root blog directory (where the manage.py file lives), sync the initial database. This runs migrations for admin, auth, contenttypes, and sessions.

python manage.py migrate

You will also need an admin user to interact with the Django admin site and the browsable API. From the terminal, run the following:

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

Set any password you like (it must be at least eight characters). Note that you might get an error that your password is too common if you set it to something like password123.

To set up your Django REST Framework API, add rest_framework and your api app to blog/blog/settings.py:

INSTALLED_APPS = [
    # code omitted for brevity
    'rest_framework',
    'api.apps.ApiConfig',
]

Adding the ApiConfig object allows you to add other configuration options to your app (see the AppConfig documentation). You won’t need to specify any other options to complete this tutorial.

Finally, start the local development server with the following command in your terminal:

python manage.py runserver

Navigate to http://127.0.0.1:8000/admin and log in to see the Django admin site. Click Users to see your new admin user or to add one or two new users.

Creating the user API for Django REST Framework

Now that you have an admin user and one or two other users, you’ll set up a user API. This will allow read-only access to the list of users and to single users from a set of API endpoints.

User serializer

Django REST Framework uses serializers to translate querysets and model instances into JSON data. Serialization also determines which data your API returns in a response to the client.

Django’s users are created from the User model defined in django.contrib.auth.

To create a serializer for the User model, add the following to blog/api/serializers.py:

from rest_framework import serializers
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username']

As shown in this example, import the User model from Django, along with the collection of serializers from Django REST framework.

Now create the UserSerializer class, which should inherit from the ModelSerializer class.

Define the model that should be associated with this serializer (model = User). The fields array indicates which fields from the model should be included in the serializer. For example, you could also add the first_name and last_name fields.

The ModelSerializer class generates serializer fields that are based on the corresponding model’s fields. This means you don’t need to manually specify any attributes for a serializer field, as these attributes are taken from the model itself.

This serializer also creates simple create() and update() methods. These can be overridden if required.

To read more about how ModelSerializer works and how to use other serializers for more control over your data, see Serializers.

User views

There are several ways to create views in Django REST Framework. For reusable functionality and to keep code DRY, use class-based views.

Django REST Framework provides several generic view classes based on the APIView class. These views are intended for the most commonly used API patterns.

For example, the ListAPIView is used for read-only endpoints and provides a get method handler. The ListCreateAPIView class is used for read–write endpoints and provides get and post method handlers.

To create a read-only view for your list of users and a read-only view for a single user, add the following to blog/api/views.py:

from rest_framework import generics
from api 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

As this code shows, import Django REST Framework’s generics collection of views, along with the User model and the UserSerializer defined in the previous step. The UserList view provides read-only access (via get) to the list of users; the UserDetail view provides read-only access (via get) to a single user.

View names should be in the following form: {ModelName}List and {ModelName}Detail for a list of objects and a single object, respectively.

For each view, the queryset variable contains the list of model instances, as returned here by User.objects.all(). The serializer_class should be set to the UserSerializer, which will serialize the User data.

You’ll set up the endpoint paths for these views in the next step.

User URL patterns

With a model, serializer, and set of views for your users, the final step is to define the endpoint paths (called “URL patterns” by Django) for each view.

First, in blog/api/urls.py, add the following:

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from api import views

urlpatterns = [
    path('users/', views.UserList.as_view()),
    path('users/<int:pk>/', views.UserDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

Here, you’ve imported Django’s path function and your api app’s collection of views.

The path function creates an element that Django uses to display a page in your app. To do this, Django first finds the correct element by matching a URL pattern (e.g., users/) to a user-requested URL. It then imports and calls the corresponding view (e.g., UserList).

The <int:pk> sequence refers to an integer value that is a primary key (pk). Django captures this part of the URL and sends it to the view as a keyword argument.

In our case, the primary key for the User is the id field, so example.com/users/1 would return the user with id equal to 1.

Before you can interact with these URL patterns (and the ones you’ll create later in this tutorial), you’ll need to add them to the Django 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 check that these pieces are working together correctly, navigate to http://127.0.0.1:8000/users in your browser to see a list of your app’s users.

User List

This tutorial uses Django REST Framework’s browsable API to illustrate the endpoints created in this tutorial. This GUI provides authentication and forms that mimic a frontend client. You can also use cURL or httpie to test your API from the terminal if you prefer.

Note the id value of your admin user, and navigate to the endpoint for that user. For example, if the id is 1, navigate to http://127.0.0.1:8000/users/1.

User Detail

In summary, Django’s model class is serialized using the UserSerializer. This serializer provides data to the UserList and UserDetail views, which are accessed using the users/ and users/<int:pk>/ URL patterns.

Creating the Post API

With a basic user API set up, you can now create a complete API for a blog, with endpoints for posts, comments, and categories. Start by creating the Post API.

Post model

In blog/api/models.py, create a Post model that inherits from Django’s Model class, 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 field types typically used in relational databases. See Models for more detailed information on Django models and field types.

Note that the ForeignKey type creates a many-to-one relationship between the current model and the model indicated in the first argument (auth.User, which is the User model you’ve been working with).

In this case, one user can be the owner of many posts, but each post can have just one owner. The owner field might be used in a frontend app to retrieve a user and display their username as the post’s author.

The related_name argument allows you to set a custom access name for the current model (posts) instead of the default (post_set). This list of posts will be added to the User serializer in the next step to complete the many-to-one relationship.

Each time you modify a model, run the following to update the database:

python manage.py makemigrations api
python manage.py migrate

Because these are Django models like the User model you’ve been working with, your posts can be modified from Django’s admin site by registering them in blog/api/admin.py:

from django.contrib import admin
from api.models import Post

admin.site.register(Post)

Note that you will be able to create posts from the browsable API later on.

For now, navigate to http://127.0.0.1:8000/admin, click Posts, and add a few posts. You’ll notice that your title and body fields in this form correspond to the CharField and TextField types as defined by the Post model.

You can also select an owner from among the existing users. When creating a post using the browsable API, you won’t need to select a user. The owner will be set automatically to the user who is currently logged in. You’ll set this up in the next step.

Post serializer

To add the Post model to the API, you’ll follow a process similar to the one you followed for the User model.

You first need to serialize the Post model data. In blog/api/serializers.py, add the following:

# code omitted for brevity
from api.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']

As this example shows, import the Post model from your api app, and create a PostSerializer that inherits from the ModelSerializer class. Set the model and fields to be used by this serializer.

The ReadOnlyField is a field class that returns a value without modification. In this case, it’s used to return the username field of the owner instead of the default id field.

Next, add a posts field to the UserSerializer. The many-to-one relationship between posts and users was defined by the Post model in the previous step. The field name (posts) should be equal to the related_field argument of the Post.owner field. Change posts to post_set (the default value) if you did not indicate a related_field value in the previous step.

The PrimaryKeyRelatedField represents the list of posts in this many-to-one relationship (many=True signifies there is more than one post).

Note that without setting read_only=True, the posts field would have write access by default. This means you’d be able to manually set the list of posts belonging to a user when that user is created. This is probably not the behavior you want.

Navigate to http://127.0.0.1:8000/users again to see the posts field for each user.

Note that the list of posts is really a list of post id values. You can instead return a list of URLs using the HyperlinkedModelSerializer.

Post views

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

# code omitted for brevity
from api.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

# code omitted for brevity

The ListCreateAPIView and RetrieveUpdateDestroyAPIView together 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 must also override the default perform_create function to set the owner field to the current user (the value of self.request.user).

Post URL patterns

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

# code omitted for brevity

urlpatterns = [
    # code omitted for brevity
    path('posts/', views.PostList.as_view()),
    path('posts/<int:pk>/', views.PostDetail.as_view()),
]

Combining the views with these URL patterns creates the get posts/, post posts/, get posts/<int:pk>/, put posts/<int:pk>/, and delete posts/<int:pk>/ endpoints.

To test these endpoints, navigate to to a single post, such as http://127.0.0.1:8000/posts/1, and click DELETE. To change the title of the post, change the title field value and click PUT to update it.

Post Detail

Navigate to http://127.0.0.1:8000/posts to see the list of existing posts or to create a new post. Make sure you’re logged in when attempting to create a post, since a post’s owner is set to the current user.

Post List

Setting permissions

For convenience, you can add a Log in button to the browsable API by adding the following path to blog/urls.py:

# code omitted for brevity

urlpatterns = [
    # code omitted for brevity
    path('api-auth/', include('rest_framework.urls')),
]

You can now log in to and out of different user accounts to test permissions and modify your posts using the browsable API.

Currently, you can create a post as long as you’re logged in, but you don’t need to be logged in to delete or modify a post — even a post that doesn’t belong to you. Try logging in with a different user account. You should be able to modify or delete a post owned by admin.

To authenticate users and ensure that only the owner of a post can update or delete an existing post, you’ll need to add permissions to your API.

First, add the following to blog/api/permissions.py:

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 code for this permission was taken from the Django REST Framework documentation.

The custom IsOwnerOrReadOnly permission checks whether the requesting user is the owner of the given object. In this case, only owners can perform actions such as updating or deleting a post. Non-owners can still retrieve a post, since this is a read-only action.

There is also a built-in IsAuthenticatedOrReadOnly permission. With this permission, any authenticated user can perform any request, whereas non-authenticated users can perform only read-only requests.

Add these permissions to the Post views:

# code omitted for brevity
from rest_framework import permissions
from api.permissions import IsOwnerOrReadOnly

class PostList(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_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
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

# code omitted for brevity

The PostList view needs only the IsAuthenticatedOrReadOnly permission because a user must be authenticated to create a post, and any user can view the list of posts.

The PostDetail requires both permissions, as updating and destroying a post should only be allowed for an authenticated user who is also the owner of the post. Retrieving a single post is read-only and does not require any permissions.

Navigate again to http://127.0.0.1:8000/posts. Log in to your admin account and other user accounts to test which actions can now be performed by authenticated and unauthenticated users.

When logged out, you should not be able to create, delete, or update any posts. When logged in as one user, you should not be able to delete or update the posts owned by a different user.

Post Detail Auth

For more information on permissions in Django REST Framework, see Permissions.

Creating the comment API

You now have a basic post API. You can now add a comment system to your posts.

A comment is text that is added by a user in response to a post and belongs to an individual user. A user can make many comments on any post, including their own, and a post can have many comments from different users. This means you will set up two many-to-one relationships: one between comments and users and one between comments and posts.

Comment model

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

# code omitted for brevity

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 the Post model and has a many-to-one relationship with users through the owner field. A comment also has a many-to-one relationship with a single post through the post field.

Run your database migrations as before:

python manage.py makemigrations api
python manage.py migrate

Comment serializer

To create the comment API, first add the Comment model to the PostSerializer and UserSerializer to ensure associated comments are sent along with other post and user data.

Add this code to blog/api/serializers.py:

# code omitted for brevity
from api.models import 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 here is similar to adding posts to the UserSerializer. Again, this sets the “many” part of the many-to-one relationship between comments and a user and between comments and a post. The list of comments should again be read-only (by setting read_only=True).

Now, add the CommentSerializer to the same file:

class CommentSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Comment
        fields = ['id', 'body', 'owner', 'post']

Notice here that you don’t need to customize the post field. By adding the post field directly to the fields array, it is serialized in the default manner (according to the ModelSerializer). This is equivalent to defining post = serializers.PrimaryKeyRelatedField(queryset=Post.objects.all()).

This means the post field has write access by default: when a user creates a new comment, they also set the post it belongs to.

Comment views

Finally, create custom views and URL patterns for your comments. This process is similar to the process you followed for the Post API.

Add this code to blog/api/views.py:

from api.models import Comment

class CommentList(generics.ListCreateAPIView):
    queryset = Comment.objects.all()
    serializer_class = serializers.CommentSerializer
    permission_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
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

These views are similar to the PostList and PostDetail views.

Comment URL patterns

To complete the comment API, define the URL patterns in blog/api/urls.py:

# code omitted for brevity

urlpatterns = [
    # code omitted for brevity
    path('comments/', views.CommentList.as_view()),
    path('comments/<int:pk>/', views.CommentDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

You can now navigate to http://127.0.0.1:8000/comments to see a list of existing comments and create new comments.

Comment List

Notice that when creating a new comment in the browsable API, you must choose the post from a list of existing posts.

Creating the category API

The final piece of your blog API is a category system.

One or more categories can be added to any post. Because a post can have many categories, and a category can belong to many posts, a many-to-many relationship must be defined.

Category model

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

class Category(models.Model):
    name = models.CharField(max_length=100, blank=False, default='')
    owner = models.ForeignKey('auth.User', related_name='categories', on_delete=models.CASCADE)
    posts = models.ManyToManyField('Post', related_name='categories', blank=True)

    class Meta:
        verbose_name_plural = 'categories'

Here, the ManyToManyField class creates a many-to-many relationship between the current model and the model indicated in the first argument. As with the ForeignKey class, this relationship is completed by the serializer.

Note that verbose_name_plural determines how to pluralize the model name in places such as the Django admin site. This avoids pluralizing category as categorys and sets the plural manually to categories.

Run your database migrations as before:

python manage.py makemigrations api
python manage.py migrate

Category serializer

The process of creating the category API is similar to the process you followed in previous steps. First, create a serializer for the Category model by adding this code to blog/api/serializers.py:

# code omitted for brevity
from api.models import 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']

Be sure to add the categories field name to the list of fields in both PostSerializer and UserSerializer. Note that UserSerializer.categories should also be customized with read_only=True. This field represents a viewable list of all categories the user has created.

On the other hand, the PostSerializer.categories field will have write access by default. The default is the same as setting categories = serializers.PrimaryKeyRelatedField(many=True, queryset=Category.objects.all()). This allows a user to select one or more existing categories to assign to a new post.

Category views

Next, create the views for the category API in blog/api/views.py:

# code omitted for brevity
from api.models import Category

class CategoryList(generics.ListCreateAPIView):
    queryset = Category.objects.all()
    serializer_class = serializers.CategorySerializer
    permission_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
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

These views are similar to the other views you’ve created so far.

Category URL patterns

Finally, to complete the category API, add this code to blog/api/urls.py:

# code omitted for brevity

urlpatterns = [
    # code omitted for brevity
    path('categories/', views.CategoryList.as_view()),
    path('categories/<int:pk>/', views.CategoryDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

You can now navigate to http://127.0.0.1:8000/categories and create one or two categories.

Category List

Next, navigate to http://127.0.0.1:8000/posts and create a new post. You’ll be able to add one or more categories to a post.

Create Post With Categories

Conclusion

Congratulations! You now have a blog API with authentication and many of the most common patterns in API development. You have created endpoints for retrieving, creating, updating, and deleting posts, comments, and categories. You’ve also added many-to-one and many-to-many relationships between these resources.

To expand your API or create a front-end client for it, refer to the Django REST Framework documentation and the Tutorials and Resources.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Kate Bartolo I am a developer and technical writer with a love of language and all things open source. I write about web development, game development, and documentation at kabartolo.com.

One Reply to “Use Django REST Framework to build a blog”

Leave a Reply