Claude-skill-registry django-rest-framework
Use when Django REST Framework for building APIs with serializers, viewsets, and authentication. Use when creating RESTful APIs.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/django-rest-framework" ~/.claude/skills/majiayu000-claude-skill-registry-django-rest-framework && rm -rf "$T"
skills/data/django-rest-framework/SKILL.mdDjango REST Framework
Master Django REST Framework for building robust, scalable RESTful APIs with proper serialization and authentication.
Serializers
Build type-safe data serialization with Django REST Framework serializers.
from rest_framework import serializers from django.contrib.auth.models import User class UserSerializer(serializers.ModelSerializer): post_count = serializers.IntegerField(read_only=True) full_name = serializers.SerializerMethodField() class Meta: model = User fields = ['id', 'email', 'name', 'post_count', 'full_name'] read_only_fields = ['id', 'created_at'] extra_kwargs = { 'email': {'required': True}, 'password': {'write_only': True} } def get_full_name(self, obj): return f"{obj.first_name} {obj.last_name}" class PostSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) author_id = serializers.IntegerField(write_only=True) class Meta: model = Post fields = '__all__' def validate_title(self, value): if len(value) < 5: raise serializers.ValidationError('Title must be at least 5 characters') return value def validate(self, data): if data.get('published') and not data.get('content'): raise serializers.ValidationError('Published posts must have content') return data def create(self, validated_data): # Custom creation logic post = Post.objects.create(**validated_data) # Send notification, etc. return post
Custom Fields and Validation
Create custom serializer fields for complex data types.
from rest_framework import serializers class Base64ImageField(serializers.ImageField): """Handle base64 encoded images.""" def to_internal_value(self, data): import base64 from django.core.files.base import ContentFile if isinstance(data, str) and data.startswith('data:image'): format, imgstr = data.split(';base64,') ext = format.split('/')[-1] data = ContentFile(base64.b64decode(imgstr), name=f'temp.{ext}') return super().to_internal_value(data) class PostSerializer(serializers.ModelSerializer): image = Base64ImageField(required=False) class Meta: model = Post fields = ['id', 'title', 'image'] # Custom validators def validate_no_profanity(value): profanity_words = ['bad', 'worse'] if any(word in value.lower() for word in profanity_words): raise serializers.ValidationError('Content contains profanity') return value class CommentSerializer(serializers.ModelSerializer): content = serializers.CharField(validators=[validate_no_profanity]) class Meta: model = Comment fields = ['id', 'content', 'created_at']
Nested Serializers
Handle complex nested relationships.
class CommentSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) class Meta: model = Comment fields = ['id', 'content', 'author', 'created_at'] class PostSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) comments = CommentSerializer(many=True, read_only=True) class Meta: model = Post fields = ['id', 'title', 'content', 'author', 'comments'] # Writable nested serializers class PostCreateSerializer(serializers.ModelSerializer): comments = CommentSerializer(many=True, required=False) class Meta: model = Post fields = ['id', 'title', 'content', 'comments'] def create(self, validated_data): comments_data = validated_data.pop('comments', []) post = Post.objects.create(**validated_data) for comment_data in comments_data: Comment.objects.create(post=post, **comment_data) return post # Dynamic nested serialization class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'content'] def __init__(self, *args, **kwargs): include_comments = kwargs.pop('include_comments', False) super().__init__(*args, **kwargs) if include_comments: self.fields['comments'] = CommentSerializer(many=True, read_only=True)
ViewSets
Create RESTful endpoints with ViewSets.
from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] filterset_fields = ['author', 'published'] search_fields = ['title', 'content'] ordering_fields = ['created_at', 'title'] def get_queryset(self): queryset = super().get_queryset() if self.action == 'list': queryset = queryset.filter(published=True) return queryset.select_related('author').prefetch_related('comments') def get_serializer_class(self): if self.action == 'create': return PostCreateSerializer return PostSerializer def perform_create(self, serializer): serializer.save(author=self.request.user) @action(detail=True, methods=['post']) def publish(self, request, pk=None): post = self.get_object() post.published = True post.save() return Response({'status': 'published'}) @action(detail=False, methods=['get']) def recent(self, request): recent_posts = self.get_queryset()[:10] serializer = self.get_serializer(recent_posts, many=True) return Response(serializer.data) # ReadOnly ViewSet class CategoryViewSet(viewsets.ReadOnlyModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer
Routers
Configure URL routing for ViewSets.
from rest_framework.routers import DefaultRouter, SimpleRouter from django.urls import path, include # Default router (with API root view) router = DefaultRouter() router.register(r'posts', PostViewSet, basename='post') router.register(r'users', UserViewSet, basename='user') router.register(r'comments', CommentViewSet, basename='comment') urlpatterns = [ path('api/', include(router.urls)), ] # Simple router (no API root) simple_router = SimpleRouter() simple_router.register(r'posts', PostViewSet) # Custom routing from rest_framework.routers import Route, DynamicRoute class CustomRouter(DefaultRouter): routes = [ Route( url=r'^{prefix}/$', mapping={'get': 'list', 'post': 'create'}, name='{basename}-list', detail=False, initkwargs={} ), # Add custom routes ]
Permissions
Implement authentication and authorization.
from rest_framework import permissions class IsAuthorOrReadOnly(permissions.BasePermission): """Custom permission to only allow authors to edit.""" def has_object_permission(self, request, view, obj): # Read permissions for any request if request.method in permissions.SAFE_METHODS: return True # Write permissions only for author return obj.author == request.user class IsOwnerOrAdmin(permissions.BasePermission): def has_object_permission(self, request, view, obj): return obj.owner == request.user or request.user.is_staff # Usage in ViewSet class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer permission_classes = [permissions.IsAuthenticated, IsAuthorOrReadOnly] # Multiple permission classes from rest_framework.permissions import IsAuthenticated, IsAdminUser class AdminPostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer def get_permissions(self): if self.action in ['create', 'update', 'partial_update', 'destroy']: return [IsAdminUser()] return [IsAuthenticated()]
Authentication
Configure various authentication methods.
from rest_framework.authentication import TokenAuthentication, SessionAuthentication from rest_framework.authtoken.models import Token from rest_framework.permissions import IsAuthenticated # Token Authentication class PostViewSet(viewsets.ModelViewSet): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] queryset = Post.objects.all() serializer_class = PostSerializer # Create token for user from rest_framework.authtoken.views import obtain_auth_token from django.urls import path urlpatterns = [ path('api-token-auth/', obtain_auth_token), ] # Custom token authentication from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.authtoken.models import Token from rest_framework.response import Response class CustomAuthToken(ObtainAuthToken): def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] token, created = Token.objects.get_or_create(user=user) return Response({ 'token': token.key, 'user_id': user.pk, 'email': user.email }) # JWT Authentication (using djangorestframework-simplejwt) from rest_framework_simplejwt.authentication import JWTAuthentication class PostViewSet(viewsets.ModelViewSet): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated]
Filtering and Search
Implement advanced filtering capabilities.
from django_filters import rest_framework as filters from rest_framework import filters as drf_filters class PostFilter(filters.FilterSet): title = filters.CharFilter(lookup_expr='icontains') created_after = filters.DateTimeFilter(field_name='created_at', lookup_expr='gte') created_before = filters.DateTimeFilter(field_name='created_at', lookup_expr='lte') class Meta: model = Post fields = ['author', 'published', 'title'] class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer filter_backends = [ filters.DjangoFilterBackend, drf_filters.SearchFilter, drf_filters.OrderingFilter ] filterset_class = PostFilter search_fields = ['title', 'content', 'author__name'] ordering_fields = ['created_at', 'title', 'views'] ordering = ['-created_at'] # Custom filter backend class IsOwnerFilterBackend(filters.BaseFilterBackend): def filter_queryset(self, request, queryset, view): return queryset.filter(owner=request.user)
Pagination
Configure pagination for large datasets.
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination class StandardResultsSetPagination(PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' max_page_size = 100 class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer pagination_class = StandardResultsSetPagination # Cursor pagination for better performance class PostCursorPagination(CursorPagination): page_size = 20 ordering = '-created_at' # Custom pagination class CustomPagination(PageNumberPagination): def get_paginated_response(self, data): return Response({ 'links': { 'next': self.get_next_link(), 'previous': self.get_previous_link() }, 'count': self.page.paginator.count, 'total_pages': self.page.paginator.num_pages, 'results': data })
Throttling
Rate limit API requests.
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle class BurstRateThrottle(UserRateThrottle): rate = '60/min' class SustainedRateThrottle(UserRateThrottle): rate = '1000/day' class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer throttle_classes = [BurstRateThrottle, SustainedRateThrottle] # Custom throttle from rest_framework.throttling import SimpleRateThrottle class UploadRateThrottle(SimpleRateThrottle): rate = '10/hour' def get_cache_key(self, request, view): if request.user.is_authenticated: ident = request.user.pk else: ident = self.get_ident(request) return self.cache_format % {'scope': self.scope, 'ident': ident}
Versioning
Handle API versioning.
from rest_framework.versioning import URLPathVersioning, NamespaceVersioning # URL path versioning class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() versioning_class = URLPathVersioning def get_serializer_class(self): if self.request.version == 'v1': return PostSerializerV1 return PostSerializerV2 # URLs urlpatterns = [ path('v1/posts/', PostViewSet.as_view({'get': 'list'})), path('v2/posts/', PostViewSet.as_view({'get': 'list'})), ] # Accept header versioning from rest_framework.versioning import AcceptHeaderVersioning REST_FRAMEWORK = { 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSION': 'v1', 'ALLOWED_VERSIONS': ['v1', 'v2'], }
Error Handling
Implement custom error responses.
from rest_framework.views import exception_handler from rest_framework.response import Response def custom_exception_handler(exc, context): response = exception_handler(exc, context) if response is not None: response.data = { 'error': { 'status_code': response.status_code, 'message': response.data, 'detail': str(exc) } } return response # settings.py REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'myapp.utils.custom_exception_handler' } # Custom exceptions from rest_framework.exceptions import APIException class ServiceUnavailable(APIException): status_code = 503 default_detail = 'Service temporarily unavailable' default_code = 'service_unavailable' # Usage from rest_framework import status from rest_framework.response import Response class PostViewSet(viewsets.ModelViewSet): def create(self, request): try: # Logic pass except Exception as e: raise ServiceUnavailable(detail=str(e))
Advanced Serializer Patterns
Master complex serialization scenarios.
from rest_framework import serializers # Dynamic field selection class DynamicFieldsModelSerializer(serializers.ModelSerializer): """Serializer that accepts 'fields' parameter to dynamically include/exclude fields.""" def __init__(self, *args, **kwargs): fields = kwargs.pop('fields', None) exclude = kwargs.pop('exclude', None) super().__init__(*args, **kwargs) if fields is not None: allowed = set(fields) existing = set(self.fields) for field_name in existing - allowed: self.fields.pop(field_name) if exclude is not None: for field_name in exclude: self.fields.pop(field_name, None) class PostSerializer(DynamicFieldsModelSerializer): class Meta: model = Post fields = '__all__' # Usage: serializer = PostSerializer(post, fields=('id', 'title', 'author')) serializer = PostSerializer(post, exclude=('content',)) # Serializer method field with context class PostSerializer(serializers.ModelSerializer): is_liked = serializers.SerializerMethodField() like_count = serializers.SerializerMethodField() class Meta: model = Post fields = ['id', 'title', 'is_liked', 'like_count'] def get_is_liked(self, obj): request = self.context.get('request') if request and request.user.is_authenticated: return obj.likes.filter(user=request.user).exists() return False def get_like_count(self, obj): return obj.likes.count() # Nested writable serializers class CommentSerializer(serializers.ModelSerializer): author_name = serializers.CharField(source='author.name', read_only=True) class Meta: model = Comment fields = ['id', 'content', 'author', 'author_name'] class PostSerializer(serializers.ModelSerializer): comments = CommentSerializer(many=True, required=False) class Meta: model = Post fields = ['id', 'title', 'content', 'comments'] def create(self, validated_data): comments_data = validated_data.pop('comments', []) post = Post.objects.create(**validated_data) for comment_data in comments_data: Comment.objects.create(post=post, **comment_data) return post def update(self, instance, validated_data): comments_data = validated_data.pop('comments', None) instance.title = validated_data.get('title', instance.title) instance.content = validated_data.get('content', instance.content) instance.save() if comments_data is not None: # Clear existing comments instance.comments.all().delete() # Create new comments for comment_data in comments_data: Comment.objects.create(post=instance, **comment_data) return instance # Polymorphic serialization class ContentSerializer(serializers.Serializer): """Base serializer for polymorphic content.""" def to_representation(self, instance): if isinstance(instance, Article): return ArticleSerializer(instance, context=self.context).data elif isinstance(instance, Video): return VideoSerializer(instance, context=self.context).data elif isinstance(instance, Image): return ImageSerializer(instance, context=self.context).data return super().to_representation(instance)
ViewSet Composition and Actions
Build sophisticated ViewSets with custom actions.
from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from django.db.models import Count, Q class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer def get_queryset(self): queryset = super().get_queryset() # Filter by query parameters author = self.request.query_params.get('author') if author: queryset = queryset.filter(author_id=author) published = self.request.query_params.get('published') if published is not None: queryset = queryset.filter(published=published == 'true') # Optimize based on action if self.action == 'list': queryset = queryset.select_related('author').only( 'id', 'title', 'created_at', 'author__name' ) elif self.action == 'retrieve': queryset = queryset.select_related('author').prefetch_related( 'comments__author', 'tags' ) return queryset def get_serializer_class(self): if self.action == 'list': return PostListSerializer elif self.action in ['create', 'update', 'partial_update']: return PostWriteSerializer return PostSerializer @action(detail=True, methods=['post']) def publish(self, request, pk=None): """Publish a post.""" post = self.get_object() post.published = True post.published_at = timezone.now() post.save() serializer = self.get_serializer(post) return Response(serializer.data) @action(detail=True, methods=['post']) def like(self, request, pk=None): """Like a post.""" post = self.get_object() user = request.user like, created = Like.objects.get_or_create(post=post, user=user) if not created: like.delete() return Response({'status': 'unliked'}) return Response({'status': 'liked'}, status=status.HTTP_201_CREATED) @action(detail=False, methods=['get']) def trending(self, request): """Get trending posts.""" posts = self.get_queryset().annotate( like_count=Count('likes') ).filter( created_at__gte=timezone.now() - timedelta(days=7) ).order_by('-like_count')[:10] serializer = self.get_serializer(posts, many=True) return Response(serializer.data) @action(detail=False, methods=['get']) def stats(self, request): """Get post statistics.""" queryset = self.get_queryset() stats = { 'total': queryset.count(), 'published': queryset.filter(published=True).count(), 'drafts': queryset.filter(published=False).count(), 'total_likes': Like.objects.filter(post__in=queryset).count() } return Response(stats) @action(detail=True, methods=['get']) def comments(self, request, pk=None): """Get comments for a post.""" post = self.get_object() comments = post.comments.select_related('author').all() page = self.paginate_queryset(comments) if page is not None: serializer = CommentSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = CommentSerializer(comments, many=True) return Response(serializer.data) def perform_create(self, serializer): """Save with current user as author.""" serializer.save(author=self.request.user) def perform_destroy(self, instance): """Soft delete instead of hard delete.""" instance.deleted_at = timezone.now() instance.save()
Advanced Permission Patterns
Implement granular permission control.
from rest_framework import permissions class IsAuthorOrReadOnly(permissions.BasePermission): """Object-level permission to only allow authors to edit.""" def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True return obj.author == request.user class IsPublishedOrAuthor(permissions.BasePermission): """Only show published posts unless user is the author.""" def has_object_permission(self, request, view, obj): if obj.published: return True return obj.author == request.user class HasAPIKey(permissions.BasePermission): """Check for valid API key in header.""" def has_permission(self, request, view): api_key = request.META.get('HTTP_X_API_KEY') if not api_key: return False return APIKey.objects.filter( key=api_key, is_active=True ).exists() class RateLimitPermission(permissions.BasePermission): """Custom rate limiting based on user tier.""" def has_permission(self, request, view): user = request.user if not user.is_authenticated: return False # Check rate limit based on user tier if user.tier == 'premium': rate = 1000 # requests per day else: rate = 100 # Implement rate limiting logic cache_key = f'rate_limit_{user.id}' current_count = cache.get(cache_key, 0) if current_count >= rate: return False cache.set(cache_key, current_count + 1, timeout=86400) return True # Combine multiple permissions class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer def get_permissions(self): if self.action in ['create', 'update', 'partial_update', 'destroy']: permission_classes = [permissions.IsAuthenticated, IsAuthorOrReadOnly] elif self.action == 'list': permission_classes = [permissions.AllowAny] else: permission_classes = [IsPublishedOrAuthor] return [permission() for permission in permission_classes]
Advanced Filtering and Search
Implement sophisticated filtering capabilities.
from django_filters import rest_framework as filters from rest_framework import filters as drf_filters class PostFilter(filters.FilterSet): # Text filters title = filters.CharFilter(lookup_expr='icontains') title_exact = filters.CharFilter(field_name='title', lookup_expr='exact') # Date range filters created_after = filters.DateTimeFilter(field_name='created_at', lookup_expr='gte') created_before = filters.DateTimeFilter(field_name='created_at', lookup_expr='lte') # Number range filters min_views = filters.NumberFilter(field_name='views', lookup_expr='gte') max_views = filters.NumberFilter(field_name='views', lookup_expr='lte') # Choice filter status = filters.ChoiceFilter(choices=( ('published', 'Published'), ('draft', 'Draft'), ('archived', 'Archived') )) # Multiple choice filter tags = filters.ModelMultipleChoiceFilter( queryset=Tag.objects.all(), field_name='tags', conjoined=False # OR instead of AND ) # Custom method filter has_comments = filters.BooleanFilter(method='filter_has_comments') class Meta: model = Post fields = ['author', 'published', 'category'] def filter_has_comments(self, queryset, name, value): if value: return queryset.filter(comments__isnull=False).distinct() return queryset.filter(comments__isnull=True) class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer filter_backends = [ filters.DjangoFilterBackend, drf_filters.SearchFilter, drf_filters.OrderingFilter ] filterset_class = PostFilter # Search configuration search_fields = [ 'title', 'content', 'author__name', '=author__username', # Exact match '@description', # Full-text search (PostgreSQL) ] # Ordering configuration ordering_fields = ['created_at', 'updated_at', 'views', 'title'] ordering = ['-created_at'] # Custom filter backend class IsOwnerFilterBackend(filters.BaseFilterBackend): """Filter objects to show only user's own objects.""" def filter_queryset(self, request, queryset, view): if not request.user.is_authenticated: return queryset.none() return queryset.filter(author=request.user) class MyPostViewSet(viewsets.ReadOnlyModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer filter_backends = [IsOwnerFilterBackend]
Pagination Strategies
Implement various pagination approaches.
from rest_framework.pagination import ( PageNumberPagination, LimitOffsetPagination, CursorPagination ) class StandardPagination(PageNumberPagination): page_size = 20 page_size_query_param = 'page_size' max_page_size = 100 def get_paginated_response(self, data): return Response({ 'links': { 'next': self.get_next_link(), 'previous': self.get_previous_link() }, 'count': self.page.paginator.count, 'total_pages': self.page.paginator.num_pages, 'current_page': self.page.number, 'results': data }) class LargeResultsPagination(PageNumberPagination): page_size = 1000 max_page_size = 10000 class SmallResultsPagination(PageNumberPagination): page_size = 10 class PostCursorPagination(CursorPagination): page_size = 20 ordering = '-created_at' cursor_query_param = 'cursor' def get_paginated_response(self, data): return Response({ 'next': self.get_next_link(), 'previous': self.get_previous_link(), 'results': data }) class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer def get_pagination_class(self): if self.action == 'list': return StandardPagination elif self.action == 'trending': return SmallResultsPagination return None pagination_class = StandardPagination
API Versioning Strategies
Manage API versions effectively.
from rest_framework.versioning import ( URLPathVersioning, NamespaceVersioning, AcceptHeaderVersioning, QueryParameterVersioning ) # URL path versioning class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() versioning_class = URLPathVersioning def get_serializer_class(self): if self.request.version == 'v1': return PostSerializerV1 elif self.request.version == 'v2': return PostSerializerV2 return PostSerializer # URLs configuration urlpatterns = [ path('v1/posts/', PostViewSet.as_view({'get': 'list'}), name='post-list-v1'), path('v2/posts/', PostViewSet.as_view({'get': 'list'}), name='post-list-v2'), ] # Accept header versioning REST_FRAMEWORK = { 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSION': 'v1', 'ALLOWED_VERSIONS': ['v1', 'v2', 'v3'], 'VERSION_PARAM': 'version', } # Version-specific serializers class PostSerializerV1(serializers.ModelSerializer): class Meta: model = Post fields = ['id', 'title', 'content'] # Minimal fields class PostSerializerV2(serializers.ModelSerializer): author = UserSerializer(read_only=True) class Meta: model = Post fields = ['id', 'title', 'content', 'author', 'created_at'] class PostSerializerV3(serializers.ModelSerializer): author = UserSerializer(read_only=True) comments = CommentSerializer(many=True, read_only=True) tags = TagSerializer(many=True, read_only=True) class Meta: model = Post fields = '__all__'
Testing DRF APIs
Write comprehensive tests for your API.
from rest_framework.test import APITestCase, APIClient, APIRequestFactory from rest_framework import status from django.contrib.auth.models import User from django.urls import reverse class PostAPITestCase(APITestCase): def setUp(self): self.client = APIClient() self.user = User.objects.create_user('testuser', 'test@test.com', 'testpass') self.client.force_authenticate(user=self.user) def test_create_post(self): data = {'title': 'Test Post', 'content': 'Test content'} response = self.client.post('/api/posts/', data) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Post.objects.count(), 1) self.assertEqual(Post.objects.get().title, 'Test Post') def test_list_posts(self): Post.objects.create(title='Post 1', author=self.user) Post.objects.create(title='Post 2', author=self.user) response = self.client.get('/api/posts/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 2) def test_update_post(self): post = Post.objects.create(title='Old Title', author=self.user) data = {'title': 'New Title'} response = self.client.patch(f'/api/posts/{post.id}/', data) self.assertEqual(response.status_code, status.HTTP_200_OK) post.refresh_from_db() self.assertEqual(post.title, 'New Title') def test_delete_post(self): post = Post.objects.create(title='Test', author=self.user) response = self.client.delete(f'/api/posts/{post.id}/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Post.objects.count(), 0) def test_unauthenticated_access(self): self.client.force_authenticate(user=None) response = self.client.post('/api/posts/', {}) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_permission_denied(self): other_user = User.objects.create_user('other', password='pass') post = Post.objects.create(title='Test', author=other_user) response = self.client.patch(f'/api/posts/{post.id}/', {'title': 'Hacked'}) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_filtering(self): Post.objects.create(title='Python Post', author=self.user, published=True) Post.objects.create(title='Django Post', author=self.user, published=False) response = self.client.get('/api/posts/?published=true') self.assertEqual(len(response.data['results']), 1) self.assertEqual(response.data['results'][0]['title'], 'Python Post') def test_search(self): Post.objects.create(title='Python Tutorial', author=self.user) Post.objects.create(title='Django Guide', author=self.user) response = self.client.get('/api/posts/?search=Python') self.assertEqual(len(response.data['results']), 1) def test_ordering(self): post1 = Post.objects.create(title='A Post', author=self.user) post2 = Post.objects.create(title='Z Post', author=self.user) response = self.client.get('/api/posts/?ordering=title') self.assertEqual(response.data['results'][0]['title'], 'A Post') response = self.client.get('/api/posts/?ordering=-title') self.assertEqual(response.data['results'][0]['title'], 'Z Post') def test_pagination(self): for i in range(25): Post.objects.create(title=f'Post {i}', author=self.user) response = self.client.get('/api/posts/') self.assertEqual(len(response.data['results']), 20) # Default page size self.assertIsNotNone(response.data['next']) def test_custom_action(self): post = Post.objects.create(title='Test', author=self.user) response = self.client.post(f'/api/posts/{post.id}/publish/') self.assertEqual(response.status_code, status.HTTP_200_OK) post.refresh_from_db() self.assertTrue(post.published) # Testing with APIRequestFactory class PostViewSetTestCase(APITestCase): def setUp(self): self.factory = APIRequestFactory() self.user = User.objects.create_user('testuser', password='testpass') def test_list_action(self): request = self.factory.get('/api/posts/') request.user = self.user view = PostViewSet.as_view({'get': 'list'}) response = view(request) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_create_action(self): data = {'title': 'Test', 'content': 'Content'} request = self.factory.post('/api/posts/', data) request.user = self.user view = PostViewSet.as_view({'post': 'create'}) response = view(request) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
When to Use This Skill
Use django-rest-framework when building modern, production-ready applications that require advanced patterns, best practices, and optimal performance.
Performance Optimization
Optimize DRF API performance for production.
from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from django.views.decorators.vary import vary_on_headers class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer def get_queryset(self): queryset = super().get_queryset() # Optimize based on action if self.action == 'list': # Minimal fields for list view queryset = queryset.select_related('author').only( 'id', 'title', 'created_at', 'author__name' ) elif self.action == 'retrieve': # Full data for detail view queryset = queryset.select_related( 'author', 'category' ).prefetch_related( 'comments__author', 'tags' ) return queryset # Cache list view for 5 minutes @method_decorator(cache_page(60 * 5)) @method_decorator(vary_on_headers('Authorization')) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) # Use only() and defer() in serializers class PostListSerializer(serializers.ModelSerializer): author_name = serializers.CharField(source='author.name', read_only=True) class Meta: model = Post fields = ['id', 'title', 'author_name', 'created_at'] class PostDetailSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) comments = CommentSerializer(many=True, read_only=True) class Meta: model = Post fields = '__all__' # Batch requests from rest_framework.response import Response from rest_framework import status class BatchCreateMixin: """Allow batch creation of objects.""" def create(self, request, *args, **kwargs): many = isinstance(request.data, list) if not many: return super().create(request, *args, **kwargs) serializer = self.get_serializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) self.perform_create(serializer) return Response(serializer.data, status=status.HTTP_201_CREATED) class PostViewSet(BatchCreateMixin, viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer
Documentation and Schema
Generate API documentation automatically.
from rest_framework import serializers, viewsets from rest_framework.decorators import action from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample from drf_spectacular.types import OpenApiTypes class PostSerializer(serializers.ModelSerializer): """Serializer for Post objects.""" class Meta: model = Post fields = ['id', 'title', 'content', 'author', 'created_at'] read_only_fields = ['id', 'created_at'] class PostViewSet(viewsets.ModelViewSet): """ ViewSet for managing posts. Provides CRUD operations for posts with additional custom actions for publishing and liking. """ queryset = Post.objects.all() serializer_class = PostSerializer @extend_schema( summary="Publish a post", description="Set the post's published status to true", responses={200: PostSerializer} ) @action(detail=True, methods=['post']) def publish(self, request, pk=None): post = self.get_object() post.published = True post.save() serializer = self.get_serializer(post) return Response(serializer.data) @extend_schema( parameters=[ OpenApiParameter( name='author', type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, description='Filter by author ID' ), OpenApiParameter( name='published', type=OpenApiTypes.BOOL, location=OpenApiParameter.QUERY, description='Filter by published status' ) ] ) def list(self, request, *args, **kwargs): """List posts with optional filtering.""" return super().list(request, *args, **kwargs) # settings.py REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } SPECTACULAR_SETTINGS = { 'TITLE': 'My API', 'DESCRIPTION': 'API for managing posts and comments', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, } # urls.py from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ]
DRF Best Practices
- Use ModelSerializer - Leverage ModelSerializer to reduce boilerplate code
- Validate at serializer level - Implement validation in serializers, not views
- Use ViewSets for standard CRUD - ViewSets reduce code duplication for standard operations
- Optimize with select_related - Always optimize queries in get_queryset()
- Version your API - Plan for versioning from the start
- Use proper permissions - Implement granular permissions at object level
- Implement pagination - Always paginate list endpoints
- Add throttling - Protect your API with rate limiting
- Use filtering backends - Enable search and filtering for better UX
- Write comprehensive tests - Test all endpoints and permission scenarios
- Cache expensive operations - Use cache decorators for list views
- Separate read/write serializers - Use different serializers for different actions
- Document your API - Use drf-spectacular or similar for auto-generated docs
- Handle errors gracefully - Provide clear error messages for API consumers
- Use bulk operations - Support batch creation/updates for better performance
DRF Common Pitfalls
- Not optimizing queries - N+1 problems in serializers accessing related objects
- Overly complex serializers - Too much logic in serializers instead of models
- Missing validation - Not validating data at both field and object level
- Inconsistent API design - Not following REST conventions
- No pagination - Returning unbounded lists causes performance issues
- Weak authentication - Not implementing proper token expiration or refresh
- Missing permissions - Not implementing object-level permissions
- No API versioning - Breaking changes affect existing clients
- Poor error messages - Generic errors that don't help API consumers
- Inadequate testing - Not testing permissions, edge cases, and error scenarios
- Exposing sensitive data - Returning password hashes or internal IDs
- Not using read_only_fields - Allowing modification of computed fields
- Ignoring CORS - Not configuring CORS for frontend applications
- Missing rate limiting - APIs vulnerable to abuse without throttling
- Not handling file uploads - Improper handling of multipart/form-data requests