Django REST Framework: Complete 2026 Guide


Django REST Framework: Complete 2026 Guide

Django REST Framework (DRF) remains the gold standard for building REST APIs in Python. This guide covers modern patterns, best practices, and production-ready configurations for 2026.

Why Django REST Framework?

  • Batteries included - Authentication, serialization, pagination built-in
  • Browsable API - Interactive documentation out of the box
  • ORM integration - Seamless Django model integration
  • Mature ecosystem - Battle-tested in production
  • Extensive documentation - Well-documented with examples

Project Setup

# Create virtual environment
python -m venv venv
source venv/bin/activate

# Install dependencies
pip install django djangorestframework django-filter drf-spectacular

# Create project
django-admin startproject config .
python manage.py startapp api

Project Structure

project/
├── config/
│   ├── __init__.py
│   ├── settings/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── development.py
│   │   └── production.py
│   ├── urls.py
│   └── wsgi.py
├── api/
│   ├── __init__.py
│   ├── models.py
│   ├── serializers.py
│   ├── views.py
│   ├── urls.py
│   ├── permissions.py
│   ├── filters.py
│   └── tests/
│       ├── __init__.py
│       ├── test_views.py
│       └── test_serializers.py
├── users/
│   ├── __init__.py
│   ├── models.py
│   ├── serializers.py
│   └── views.py
├── manage.py
└── requirements.txt

Configuration

Settings

# config/settings/base.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Third party
    'rest_framework',
    'rest_framework.authtoken',
    'django_filters',
    'drf_spectacular',
    'corsheaders',
    # Local apps
    'api',
    'users',
]

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

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/hour',
        'user': '1000/hour',
    },
    'EXCEPTION_HANDLER': 'api.exceptions.custom_exception_handler',
}

SPECTACULAR_SETTINGS = {
    'TITLE': 'My API',
    'DESCRIPTION': 'API documentation',
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': False,
}

# JWT Settings
from datetime import timedelta
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
}

Models

# api/models.py
from django.db import models
from django.conf import settings
import uuid

class TimeStampedModel(models.Model):
    """Abstract base model with timestamps"""
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

class Category(TimeStampedModel):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(max_length=100, unique=True)
    description = models.TextField(blank=True)

    class Meta:
        verbose_name_plural = 'categories'
        ordering = ['name']

    def __str__(self):
        return self.name

class Article(TimeStampedModel):
    class Status(models.TextChoices):
        DRAFT = 'draft', 'Draft'
        PUBLISHED = 'published', 'Published'
        ARCHIVED = 'archived', 'Archived'

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    title = models.CharField(max_length=255)
    slug = models.SlugField(max_length=255, unique=True)
    content = models.TextField()
    excerpt = models.TextField(max_length=500, blank=True)
    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.DRAFT
    )
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='articles'
    )
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        related_name='articles'
    )
    tags = models.ManyToManyField('Tag', related_name='articles', blank=True)
    featured = models.BooleanField(default=False)
    views = models.PositiveIntegerField(default=0)
    published_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['status', '-published_at']),
            models.Index(fields=['author', '-created_at']),
        ]

    def __str__(self):
        return self.title

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=50, unique=True)

    def __str__(self):
        return self.name

class Comment(TimeStampedModel):
    article = models.ForeignKey(
        Article,
        on_delete=models.CASCADE,
        related_name='comments'
    )
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='comments'
    )
    content = models.TextField()
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='replies'
    )
    is_approved = models.BooleanField(default=True)

    class Meta:
        ordering = ['created_at']

    def __str__(self):
        return f'Comment by {self.author} on {self.article}'

Serializers

# api/serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Article, Category, Tag, Comment

User = get_user_model()

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

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ['id', 'name', 'slug']
        read_only_fields = ['slug']

class CategorySerializer(serializers.ModelSerializer):
    article_count = serializers.SerializerMethodField()

    class Meta:
        model = Category
        fields = ['id', 'name', 'slug', 'description', 'article_count']
        read_only_fields = ['slug']

    def get_article_count(self, obj):
        return obj.articles.filter(status='published').count()

class CommentSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    replies = serializers.SerializerMethodField()

    class Meta:
        model = Comment
        fields = ['id', 'content', 'author', 'parent', 'replies', 'created_at']
        read_only_fields = ['author']

    def get_replies(self, obj):
        if obj.replies.exists():
            return CommentSerializer(obj.replies.all(), many=True).data
        return []

class ArticleListSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    category = CategorySerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    comment_count = serializers.SerializerMethodField()

    class Meta:
        model = Article
        fields = [
            'id', 'title', 'slug', 'excerpt', 'status',
            'author', 'category', 'tags', 'featured',
            'views', 'comment_count', 'published_at', 'created_at'
        ]

    def get_comment_count(self, obj):
        return obj.comments.filter(is_approved=True).count()

class ArticleDetailSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)
    category = CategorySerializer(read_only=True)
    tags = TagSerializer(many=True, read_only=True)
    comments = serializers.SerializerMethodField()

    class Meta:
        model = Article
        fields = [
            'id', 'title', 'slug', 'content', 'excerpt', 'status',
            'author', 'category', 'tags', 'featured',
            'views', 'comments', 'published_at', 'created_at', 'updated_at'
        ]

    def get_comments(self, obj):
        comments = obj.comments.filter(is_approved=True, parent=None)
        return CommentSerializer(comments, many=True).data

class ArticleCreateUpdateSerializer(serializers.ModelSerializer):
    tags = serializers.PrimaryKeyRelatedField(
        queryset=Tag.objects.all(),
        many=True,
        required=False
    )
    category = serializers.PrimaryKeyRelatedField(
        queryset=Category.objects.all(),
        required=False,
        allow_null=True
    )

    class Meta:
        model = Article
        fields = [
            'title', 'content', 'excerpt', 'status',
            'category', 'tags', 'featured', 'published_at'
        ]

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

    def create(self, validated_data):
        tags = validated_data.pop('tags', [])
        validated_data['author'] = self.context['request'].user
        
        # Auto-generate slug
        from django.utils.text import slugify
        validated_data['slug'] = slugify(validated_data['title'])
        
        article = Article.objects.create(**validated_data)
        article.tags.set(tags)
        return article

    def update(self, instance, validated_data):
        tags = validated_data.pop('tags', None)
        
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        
        if tags is not None:
            instance.tags.set(tags)
        
        return instance

Views

```python

api/views.py

from rest_framework import viewsets, status, filters from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly from django_filters.rest_framework import DjangoFilterBackend from django.utils import timezone from drf_spectacular.utils import extend_schema, OpenApiParameter

from .models import Article, Category, Tag, Comment from .serializers import ( ArticleListSerializer, ArticleDetailSerializer, ArticleCreateUpdateSerializer, CategorySerializer, TagSerializer, CommentSerializer, ) from .permissions import IsAuthorOrReadOnly from .filters import ArticleFilter

class ArticleViewSet(viewsets.ModelViewSet): “”” ViewSet for Article CRUD operations. “”” queryset = Article.objects.select_related(‘author’, ‘category’).prefetch_related(‘tags’) permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly] filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_class = ArticleFilter search_fields = [‘title’, ‘content’, ‘excerpt’] ordering_fields = [‘created_at’, ‘published_at’, ‘views’, ‘title’] ordering = [‘-created_at’] lookup_field = ‘slug’

def get_serializer_class(self):
    if self.action == 'list':
        return ArticleListSerializer
    if self.action == 'retrieve':
        return ArticleDetailSerializer
    return ArticleCreateUpdateSerializer

def get_queryset(self):
    queryset = super().get_queryset()
    
    # Non-authenticated users only see published articles
    if not self.request.user.is_authenticated:
        queryset = queryset.filter(status=Article.Status.PUBLISHED)
    elif not self.request.user.is_staff:
        # Authenticated non-staff see published + their own
        from django.db.models import Q
        queryset = queryset.filter(
            Q(status=Article.Status.PUBLISHED) |
            Q(author=self.request.user)
        )
    
    return queryset

def retrieve(self, request, *args, **kwargs):
    instance = self.get_object()
    # Increment view count
    Article.objects.filter(pk=instance.pk).update(views=models.F('views') + 1)
    serializer = self.get_serializer(instance)
    return Response(serializer.data)

@extend_schema(
    parameters=[
        OpenApiParameter(name='days', type=int, description='Number of days')
    ]
)
@action(detail=False, methods=['get'])
def featured(self, request):
    """Get featured articles"""
    articles = self.get_queryset().filter(
        featured=True,
        status=Article.Status.PUBLISHED
    )[:10]
    serializer = ArticleListSerializer(articles, many=True)
    return Response(serializer.data)

@action(detail=False, methods=['get'])
def my_articles(self, request):
    """Get current user's articles"""
    articles = self.get_queryset().filter(author=request.user)
    page = self.paginate_queryset(articles)
    
    if page is not None:
        serializer = ArticleListSerializer(page, many=True)
        return self.get_paginated_response(serializer.data)
    
    serializer = ArticleListSerializer(articles, many=True)
    return Response(serializer.data)

@action(detail=True, methods=['post'])
def publish(self, request, slug=None):
    """Publish an article"""
    article = self.get_object()
    
    if article.author != request.user and not request.user.is_staff:
        return Response(
            {'error': 'Permission denied'},
            status=status.HTTP_403_FORBIDDEN
        )
    
    article.status = Article.Status.PUBLISHED
    article.published_at = timezone.now()
    article.save()
    
    return Response({'status': 'published'})

class CategoryViewSet(viewsets.ModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)