Build REST APIs with Django REST Framework: A Complete Tutorial

Learn how to build REST APIs with Django REST Framework. This tutorial covers serializers, views, authentication, permissions, and code examples for Python developers.

Django REST Framework (DRF) is one of the most popular tools for building web APIs in Python. If you have worked with Django before, you know how the framework handles database operations, URL routing, and template rendering. DRF adds API capabilities to Django, so you can create RESTful services for mobile apps, single-page applications, or any client that needs backend data.

This tutorial walks you through building a complete REST API with Django REST Framework. By the end, you will understand how to create models, serialize data, build views, handle authentication, and secure your API with permissions. The example API manages articles with CRUD operations, user authentication, and role-based access control.

Setting Up Your Django REST Framework Project

Before diving into code, set up a Django project with Django REST Framework installed. If you already have a Django project running, skip the project creation steps and install the required packages.

First, create a new Django project and install the necessary dependencies. You need Django, Django REST Framework, and django-filter for filtering:

pip install django djangorestframework django-filter

Add rest_framework and django_filters to your INSTALLED_APPS setting in the Django settings file. The configuration also sets default authentication classes, permission classes, and pagination:

INSTALLED_APPS = [
    'rest_framework',
    'django_filters',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
}

Token authentication requires running database migrations to create the necessary tables. After adding the apps to your settings, run the migration commands to set up the database schema:

python manage.py migrate

This creates the token table and any other pending migrations for your project. The token table stores authentication tokens that clients use to identify themselves in subsequent requests.

Defining Your Data Models

Models represent the data structure of your application. For this tutorial, build an Article model that stores title, content, author information, category, publication status, and timestamps. The model uses Django’s built-in User model for authors:

from django.db import models
from django.contrib.auth.models import User

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='articles')
    category = models.CharField(max_length=50)
    published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return self.title

The model includes several important features. The related_name='articles' on the author field lets you access all articles written by a user through user.articles.all(). The auto_now_add and auto_now fields handle timestamp tracking automatically without requiring manual updates. After defining the model, generate and run migrations to create the corresponding database table.

Creating Serializers for Data Conversion

Serializers convert between complex data types (like Django models) and Python primitives that can be rendered into JSON or XML. DRF provides two main serializer types: ModelSerializer for automatic field generation based on your models, and Serializer for complete control over field definitions.

For the Article model, create a ModelSerializer that generates fields based on the model structure. The serializer includes validation rules and read-only fields for computed values:

from rest_framework import serializers

class ArticleSerializer(serializers.ModelSerializer):
    author_name = serializers.ReadOnlyField(source='author.username')

    class Meta:
        model = Article
        fields = ['id', 'title', 'content', 'author', 'author_name',
                  'category', 'published', 'created_at', 'updated_at']
        read_only_fields = ['author', 'created_at', 'updated_at']

    def validate_title(self, value):
        if len(value) < 5:
            raise serializers.ValidationError("Title must be at least 5 characters")
        return value

The author_name field shows how to include computed values in your API responses. This field reads the username from the related User model without requiring additional database queries when using select_related() in your views. The validate_title method adds custom validation that runs during the deserialization process, ensuring all article titles meet minimum length requirements.

You might also need serializers for User objects, especially when building APIs that allow user registration or profile management. DRF provides a HyperlinkedModelSerializer that includes URL fields instead of primary keys:

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

class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ['url', 'username', 'email', 'is_staff']

Building Views and ViewSets for API Endpoints

Views determine how your API responds to HTTP requests. DRF offers several view types, from simple function-based views to ViewSets that handle entire CRUD operations with minimal code. For most applications, ModelViewSet provides the best balance of functionality and simplicity.

The ArticleViewSet handles all standard CRUD operations: listing articles, creating new ones, retrieving individual articles, updating them, and deleting them. The view also integrates filtering, searching, and ordering capabilities:

from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.select_related('author').all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['category', 'published', 'author']
    search_fields = ['title', 'content', 'author__username']
    ordering_fields = ['created_at', 'updated_at', 'title']
    ordering = ['-created_at']

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

The select_related() optimization prevents additional database queries when accessing the author information in each article. The perform_create() method sets the article author to the currently authenticated user, ensuring data integrity without requiring the client to submit author IDs.

You can add custom actions to viewsets using the @action decorator. These actions create additional endpoints beyond the standard CRUD operations. For example, a publish action lets authenticated users publish their articles:

@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def publish(self, request, pk=None):
    article = self.get_object()
    if article.author != request.user:
        return Response(
            {'error': 'Only the author can publish this article'},
            status=status.HTTP_403_FORBIDDEN
        )
    article.published = True
    article.save()
    return Response({'status': 'article published'})

This custom action creates a POST endpoint at /api/articles/{id}/publish/ that only the article author can access. The action checks ownership before allowing the publication, returning a 403 error if someone else tries to publish the article.

The filter backends enable clients to narrow down results based on specific criteria. The DjangoFilterBackend allows exact matches on specified fields, while SearchFilter performs text searches across multiple fields. Clients can combine these filters in their requests:

# Filter by category
GET /api/articles/?category=python

# Search in title and content
GET /api/articles/?search=django

# Combine filters
GET /api/articles/?category=python&published=true&search=rest

# Order results
GET /api/articles/?ordering=-created_at

The search functionality uses case-insensitive partial matching, so searching for “django” will find articles containing “Django”, “django”, or “DJANGO” in their title, content, or author username.

Custom actions extend the basic CRUD operations with endpoint-specific functionality. The publish action allows authors to publish their articles directly through the API, while my_articles returns only the articles belonging to the current user:

@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
    article = self.get_object()
    if article.author != request.user:
        return Response(
            {'error': 'Only author can publish'},
            status=status.HTTP_403_FORBIDDEN
        )
    article.published = True
    article.save()
    return Response({'status': 'article published'})

@action(detail=False)
def my_articles(self, request):
    articles = Article.objects.filter(author=request.user)
    page = self.paginate_queryset(articles)
    if page is not None:
        serializer = self.get_serializer(page, many=True)
        return self.get_paginated_response(serializer.data)
    serializer = self.get_serializer(articles, many=True)
    return Response(serializer.data)

Implementing Authentication and Permissions

Authentication identifies who is making a request, while permissions determine whether that request should be allowed. DRF supports multiple authentication schemes including token-based authentication, session authentication, and OAuth 2.0 through third-party packages.

Token authentication requires adding rest_framework.authtoken to your INSTALLED_APPS and running migrations. After setup, you can create tokens for users programmatically or automatically through Django signals:

from rest_framework.authtoken.models import Token

# Create a token manually
token = Token.objects.create(user=user)
print(token.key)
# Output: '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b'

For automatic token generation, create a signal that generates a token whenever a new user is created. Add this to your app’s ready method or a signals file:

from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

Include the token authentication endpoint in your URL configuration to allow clients to obtain tokens using their credentials:

from django.urls import path
from rest_framework.authtoken import views

urlpatterns = [
    path('api-token-auth/', views.obtain_auth_token),
]

Custom permissions extend the base permission classes to implement specific authorization rules. The IsAuthorOrReadOnly permission allows anyone to read articles but restricts modifications to the original author:

from rest_framework import permissions

class IsAuthorOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.author == request.user

When testing your API, include the authentication token in the Authorization header for each request. The header format uses the literal string “Token” followed by the actual token value:

curl -X GET http://127.0.0.1:8000/api/articles/ \
     -H 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b'

Configuring URL Routing with Routers

DRF routers automatically generate URL patterns for your viewsets, reducing boilerplate and ensuring consistent URL structures. The DefaultRouter creates a complete set of endpoints for each registered viewset, including list, detail, create, update, partial update, and delete operations.

Register your viewsets with the router and include the generated URLs in your project’s URL configuration:

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r'articles', views.ArticleViewSet)
router.register(r'users', views.UserViewSet)

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

The router generates the following URL patterns for articles:

  • GET /api/articles/ - List all articles
  • POST /api/articles/ - Create a new article
  • GET /api/articles/{id}/ - Retrieve a specific article
  • PUT /api/articles/{id}/ - Update an article completely
  • PATCH /api/articles/{id}/ - Update an article partially
  • DELETE /api/articles/{id}/ - Delete an article
  • POST /api/articles/{id}/publish/ - Custom publish action

The rest_framework.urls inclusion adds login and logout views for the browsable API, useful during development and testing. If you prefer using URL namespaces for better organization, pass a namespace tuple to the include function.

Summary and Next Steps

This tutorial covered the core components of Django REST Framework: models for data structure, serializers for data conversion, viewsets for request handling, authentication for user identification, permissions for access control, and routers for URL generation.

For production deployments, add security measures like HTTPS enforcement, rate limiting, and JWT authentication. DRF also supports OpenAPI schema generation for documentation, which helps API consumers understand your endpoints.

Extend this API by adding more models, creating relationships between resources, or implementing caching for better performance. The framework lets you add custom renderers, parsers, and throttling classes as your needs grow.

Spread The Article

Share this guide

Send this article to your network or keep a copy of the direct link.

X Facebook LinkedIn Reddit Telegram

Discussion

Leave a comment

No comments yet

Be the first to start the conversation.