Django REST Framework: Complete 2026 Guide
in Development on Python, Django, Rest-api, Backend
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
이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)
