All topics
Backend · Learning hub

Django REST Framework notes for developers

Master Django REST Framework with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Backend notes
Django REST Framework

Serializers & Views

Django REST Framework: Serializers & Views Django REST Framework (DRF) adds powerful tools for building REST APIs on top of Django. Core concepts: Serializers (

Django REST Framework: Serializers & Views

Django REST Framework (DRF) adds powerful tools for building REST APIs on top of Django. Core concepts: Serializers (data validation + transformation), Views (request handling), and Routers (URL routing).

Setup

pip install djangorestframework

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

Serializers

from rest_framework import serializers
from .models import Article

# ModelSerializer — automatically maps model fields
class ArticleSerializer(serializers.ModelSerializer):
    author_name = serializers.SerializerMethodField()  # computed field

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

    def get_author_name(self, obj):
        return obj.author.get_full_name()

    def validate_title(self, value):
        if len(value) < 5:
            raise serializers.ValidationError("Title too short.")
        return value

    def validate(self, attrs):
        # Cross-field validation
        if attrs.get('published') and not attrs.get('content'):
            raise serializers.ValidationError("Published articles must have content.")
        return attrs

    def create(self, validated_data):
        validated_data['author'] = self.context['request'].user
        return super().create(validated_data)

# Nested serializer
class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ['id', 'text', 'created_at']

class ArticleDetailSerializer(ArticleSerializer):
    comments = CommentSerializer(many=True, read_only=True)

    class Meta(ArticleSerializer.Meta):
        fields = ArticleSerializer.Meta.fields + ['comments']

# Usage
serializer = ArticleSerializer(article)           # serialize single
serializer = ArticleSerializer(queryset, many=True)  # serialize list
serializer = ArticleSerializer(data=request.data)    # deserialize
if serializer.is_valid():
    serializer.save()
else:
    print(serializer.errors)

Class-Based Views

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

class ArticleListView(APIView):
    def get(self, request):
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = ArticleSerializer(data=request.data, context={'request': request})
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class ArticleDetailView(APIView):
    def get_object(self, pk):
        try:
            return Article.objects.get(pk=pk)
        except Article.DoesNotExist:
            raise Http404

    def get(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article)
        return Response(serializer.data)

    def put(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article, data=request.data, context={'request': request})
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def patch(self, request, pk):
        article = self.get_object(pk)
        serializer = ArticleSerializer(article, data=request.data, partial=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        article = self.get_object(pk)
        article.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

# urls.py
urlpatterns = [
    path('articles/', ArticleListView.as_view()),
    path('articles/<int:pk>/', ArticleDetailView.as_view()),
]

Generic Views

from rest_framework import generics

# Pre-built CRUD views — less boilerplate
class ArticleListCreateView(generics.ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    def get_queryset(self):
        # Dynamic filtering
        return Article.objects.filter(author=self.request.user)

class ArticleDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

# Other generics:
# ListAPIView, CreateAPIView
# RetrieveAPIView, UpdateAPIView, DestroyAPIView
Django REST Framework

Authentication, Permissions & Filtering

Django REST Framework: Authentication, Permissions & Filtering Authentication # Per-view authentication override from rest_framework.authentication import Token

Django REST Framework: Authentication, Permissions & Filtering

Authentication

# Per-view authentication override
from rest_framework.authentication import TokenAuthentication, SessionAuthentication

class ArticleView(APIView):
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

# Token authentication setup
# INSTALLED_APPS: add 'rest_framework.authtoken'
# python manage.py migrate

# Generate token
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=user)
# Client sends: Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4

# JWT (recommended for production)
# pip install djangorestframework-simplejwt
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('auth/token/', TokenObtainPairView.as_view()),
    path('auth/token/refresh/', TokenRefreshView.as_view()),
]

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

# Client sends: Authorization: Bearer <access_token>

# Session authentication (for browser-based APIs / browsable API)
# Already included in defaults — works with Django login

Permissions

from rest_framework.permissions import BasePermission, IsAuthenticated, IsAdminUser, AllowAny

# Custom permission
class IsAuthorOrReadOnly(BasePermission):
    def has_permission(self, request, view):
        # Allow read for anyone
        if request.method in ('GET', 'HEAD', 'OPTIONS'):
            return True
        return request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        # Read allowed for all authenticated
        if request.method in ('GET', 'HEAD', 'OPTIONS'):
            return True
        # Write only if owner
        return obj.author == request.user

class ArticleView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]

# Common built-in permissions:
# AllowAny           — no restriction
# IsAuthenticated    — must be logged in
# IsAdminUser        — staff only
# IsAuthenticatedOrReadOnly — read for all, write requires auth

Filtering, Search & Ordering

pip install django-filter
# settings.py
REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
}

class ArticleListView(generics.ListAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filterset_fields = ['author', 'status']  # ?author=1&status=published
    search_fields = ['title', 'content']     # ?search=keyword (icontains)
    ordering_fields = ['created_at', 'title']  # ?ordering=-created_at
    ordering = ['-created_at']  # default ordering

# Custom FilterSet
import django_filters

class ArticleFilter(django_filters.FilterSet):
    created_after = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
    title = django_filters.CharFilter(lookup_expr='icontains')

    class Meta:
        model = Article
        fields = ['author', 'status', 'title', 'created_after']

class ArticleListView(generics.ListAPIView):
    filterset_class = ArticleFilter
Django REST Framework

Routers, Pagination & ViewSets

Django REST Framework: Routers, Pagination & ViewSets ViewSets & Routers ViewSets combine the logic for a set of related views in a single class. Routers automa

Django REST Framework: Routers, Pagination & ViewSets

ViewSets & Routers

ViewSets combine the logic for a set of related views in a single class. Routers automatically generate URL patterns for ViewSets.

from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]

    def get_queryset(self):
        return Article.objects.filter(author=self.request.user)

    def get_serializer_class(self):
        if self.action == 'retrieve':
            return ArticleDetailSerializer
        return ArticleSerializer

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

    # Custom action
    @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
    def publish(self, request, pk=None):
        article = self.get_object()
        article.status = 'published'
        article.save()
        return Response({'status': 'published'})

    @action(detail=False, methods=['get'])
    def featured(self, request):
        featured = Article.objects.filter(is_featured=True)
        serializer = self.get_serializer(featured, many=True)
        return Response(serializer.data)

# urls.py
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register('articles', ArticleViewSet, basename='article')
# Generates:
# GET/POST    /articles/
# GET/PUT/PATCH/DELETE /articles/<pk>/
# POST        /articles/<pk>/publish/
# GET         /articles/featured/

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

# ReadOnlyModelViewSet — only list() and retrieve()
class PublicArticleViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Article.objects.filter(status='published')
    serializer_class = ArticleSerializer
    permission_classes = [AllowAny]

Pagination

# settings.py (global)
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

# Custom pagination classes
from rest_framework.pagination import PageNumberPagination, CursorPagination

class LargeResultsSetPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'page_size'  # ?page_size=50
    max_page_size = 1000

class ArticleCursorPagination(CursorPagination):
    page_size = 20
    ordering = '-created_at'   # must be unique, stable field

# Response format (PageNumberPagination):
# {
#   "count": 1023,
#   "next": "http://api.example.com/articles/?page=2",
#   "previous": null,
#   "results": [...]
# }

# Per-view pagination
class ArticleViewSet(viewsets.ModelViewSet):
    pagination_class = LargeResultsSetPagination

Throttling & Versioning

# Throttling (rate limiting)
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day',
    }
}

# Per-view throttle
class ArticleView(APIView):
    throttle_classes = [UserRateThrottle]

# URL versioning
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    'DEFAULT_VERSION': 'v1',
    'ALLOWED_VERSIONS': ['v1', 'v2'],
    'VERSION_PARAM': 'version',
}

# urls.py
urlpatterns = [
    path('api/<version>/articles/', ArticleListView.as_view()),
]

# In view
class ArticleListView(APIView):
    def get(self, request, *args, **kwargs):
        if request.version == 'v2':
            serializer = ArticleV2Serializer(...)
        else:
            serializer = ArticleSerializer(...)

Best Practices

  • Use ModelViewSet + Router for standard CRUD — eliminates repetitive URL and view code.

  • Return appropriate HTTP status codes: 201 Created, 204 No Content for delete, 400 for validation errors.

  • Use select_related/prefetch_related in get_queryset() to avoid N+1 queries.

  • Handle exceptions with custom exception handlers in settings: EXCEPTION_HANDLER.

  • Use DRF Spectacular or drf-yasg to auto-generate OpenAPI/Swagger docs from your views.

  • Test with APIClient: client = APIClient(); client.force_authenticate(user=user); client.get("/api/articles/")

  • Throttle unauthenticated endpoints to prevent abuse — default anon: 100/day is a good start.

Keep your Django REST Framework knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever