Django REST Framework

Build powerful APIs with Django REST Framework, serializers, and views

# Django REST Framework Best Practices

Comprehensive guide for building robust, scalable REST APIs with Django REST Framework (DRF) and advanced Django patterns.

---

## Core DRF Architecture

1. **ViewSet and Generic View Patterns**
   - Use ViewSets for standard CRUD operations
   - Implement Generic Views for custom endpoints
   - Leverage mixins for composable functionality
   - Example ViewSet implementation:
     ```python
     from rest_framework import viewsets, status
     from rest_framework.decorators import action
     from rest_framework.response import Response
     from rest_framework.permissions import IsAuthenticated
     from django.shortcuts import get_object_or_404

     class UserViewSet(viewsets.ModelViewSet):
         queryset = User.objects.all()
         serializer_class = UserSerializer
         permission_classes = [IsAuthenticated]
         filterset_fields = ['is_active', 'role']
         search_fields = ['username', 'email', 'first_name', 'last_name']
         ordering_fields = ['created_at', 'username']

         def get_serializer_class(self):
             if self.action == 'create':
                 return UserCreateSerializer
             elif self.action in ['update', 'partial_update']:
                 return UserUpdateSerializer
             return UserSerializer

         def get_queryset(self):
             queryset = super().get_queryset()
             if self.action == 'list':
                 return queryset.select_related('profile').prefetch_related('groups')
             return queryset

         @action(detail=True, methods=['post'])
         def set_password(self, request, pk=None):
             user = self.get_object()
             serializer = PasswordChangeSerializer(data=request.data)
             if serializer.is_valid():
                 user.set_password(serializer.validated_data['password'])
                 user.save()
                 return Response({'message': 'Password updated successfully'})
             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

         @action(detail=False, methods=['get'])
         def me(self, request):
             serializer = self.get_serializer(request.user)
             return Response(serializer.data)
     ```

2. **Serializer Design Patterns**
   - Create focused serializers for different operations
   - Implement proper validation at field and object levels
   - Use nested serializers for complex relationships
   - Example serializer patterns:
     ```python
     from rest_framework import serializers
     from django.contrib.auth.password_validation import validate_password
     from django.core.exceptions import ValidationError

     class UserSerializer(serializers.ModelSerializer):
         full_name = serializers.SerializerMethodField()
         profile = ProfileSerializer(read_only=True)
         groups = GroupSerializer(many=True, read_only=True)

         class Meta:
             model = User
             fields = ['id', 'username', 'email', 'first_name', 'last_name',
                      'full_name', 'is_active', 'date_joined', 'profile', 'groups']
             read_only_fields = ['id', 'date_joined']

         def get_full_name(self, obj):
             return f"{obj.first_name} {obj.last_name}".strip()

     class UserCreateSerializer(serializers.ModelSerializer):
         password = serializers.CharField(write_only=True, validators=[validate_password])
         password_confirm = serializers.CharField(write_only=True)
         profile = ProfileCreateSerializer(required=False)

         class Meta:
             model = User
             fields = ['username', 'email', 'first_name', 'last_name',
                      'password', 'password_confirm', 'profile']

         def validate(self, attrs):
             if attrs['password'] != attrs['password_confirm']:
                 raise serializers.ValidationError("Password fields didn't match.")
             return attrs

         def create(self, validated_data):
             profile_data = validated_data.pop('profile', None)
             validated_data.pop('password_confirm')

             user = User.objects.create_user(**validated_data)

             if profile_data:
                 Profile.objects.create(user=user, **profile_data)

             return user

     class UserUpdateSerializer(serializers.ModelSerializer):
         profile = ProfileUpdateSerializer(required=False)

         class Meta:
             model = User
             fields = ['first_name', 'last_name', 'email', 'profile']

         def update(self, instance, validated_data):
             profile_data = validated_data.pop('profile', None)

             # Update user fields
             for attr, value in validated_data.items():
                 setattr(instance, attr, value)
             instance.save()

             # Update profile if provided
             if profile_data:
                 profile = instance.profile
                 for attr, value in profile_data.items():
                     setattr(profile, attr, value)
                 profile.save()

             return instance
     ```

3. **Advanced Validation Patterns**
   - Implement custom field validators
   - Create object-level validation
   - Handle conditional validation logic
   - Example validation implementations:
     ```python
     from rest_framework import serializers
     from django.core.validators import RegexValidator

     class CustomValidationSerializer(serializers.ModelSerializer):
         phone_number = serializers.CharField(
             validators=[RegexValidator(
                 regex=r'^\+?1?\d{9,15}$',
                 message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed."
             )]
         )

         def validate_email(self, value):
             """Field-level validation for email"""
             if User.objects.filter(email=value).exclude(pk=self.instance.pk if self.instance else None).exists():
                 raise serializers.ValidationError("Email address must be unique.")
             return value

         def validate(self, attrs):
             """Object-level validation"""
             # Conditional validation
             if attrs.get('role') == 'admin' and not attrs.get('is_staff'):
                 raise serializers.ValidationError(
                     "Admin users must have staff privileges."
                 )

             # Cross-field validation
             start_date = attrs.get('start_date')
             end_date = attrs.get('end_date')
             if start_date and end_date and start_date >= end_date:
                 raise serializers.ValidationError(
                     "End date must be after start date."
                 )

             return attrs
     ```

---

## Authentication and Permissions

4. **Authentication Strategy Implementation**
   - Configure multiple authentication backends
   - Implement JWT authentication with refresh tokens
   - Handle authentication errors gracefully
   - Example authentication setup:
     ```python
     # settings.py
     REST_FRAMEWORK = {
         'DEFAULT_AUTHENTICATION_CLASSES': [
             'rest_framework_simplejwt.authentication.JWTAuthentication',
             'rest_framework.authentication.SessionAuthentication',
             'rest_framework.authentication.TokenAuthentication',
         ],
         'DEFAULT_PERMISSION_CLASSES': [
             'rest_framework.permissions.IsAuthenticated',
         ],
     }

     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,
         'UPDATE_LAST_LOGIN': True,
         'ALGORITHM': 'HS256',
         'SIGNING_KEY': SECRET_KEY,
         'AUTH_HEADER_TYPES': ('Bearer',),
     }

     # Custom JWT views
     from rest_framework_simplejwt.views import TokenObtainPairView
     from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

     class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
         def validate(self, attrs):
             data = super().validate(attrs)

             # Add custom data to token response
             data['user'] = {
                 'id': self.user.id,
                 'username': self.user.username,
                 'email': self.user.email,
                 'is_staff': self.user.is_staff,
             }

             return data

     class CustomTokenObtainPairView(TokenObtainPairView):
         serializer_class = CustomTokenObtainPairSerializer
     ```

5. **Custom Permission Classes**
   - Create reusable permission classes
   - Implement object-level permissions
   - Handle complex permission logic
   - Example permission implementations:
     ```python
     from rest_framework import permissions

     class IsOwnerOrReadOnly(permissions.BasePermission):
         """Custom permission to only allow owners to edit objects."""

         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 to the owner
             return obj.owner == request.user

     class IsAdminOrReadOnly(permissions.BasePermission):
         """Custom permission for admin-only write access."""

         def has_permission(self, request, view):
             if request.method in permissions.SAFE_METHODS:
                 return True
             return request.user.is_staff

     class RoleBasedPermission(permissions.BasePermission):
         """Permission based on user roles."""

         role_permissions = {
             'admin': ['create', 'read', 'update', 'delete'],
             'manager': ['create', 'read', 'update'],
             'user': ['read'],
         }

         def has_permission(self, request, view):
             if not request.user.is_authenticated:
                 return False

             user_role = getattr(request.user, 'role', 'user')
             allowed_actions = self.role_permissions.get(user_role, [])

             action_map = {
                 'GET': 'read',
                 'POST': 'create',
                 'PUT': 'update',
                 'PATCH': 'update',
                 'DELETE': 'delete',
             }

             required_action = action_map.get(request.method)
             return required_action in allowed_actions
     ```

6. **Rate Limiting and Throttling**
   - Implement different throttle rates for different user types
   - Create custom throttle classes
   - Handle throttle exceeded scenarios
   - Example throttling configuration:
     ```python
     # settings.py
     REST_FRAMEWORK = {
         'DEFAULT_THROTTLE_CLASSES': [
             'rest_framework.throttling.AnonRateThrottle',
             'rest_framework.throttling.UserRateThrottle'
         ],
         'DEFAULT_THROTTLE_RATES': {
             'anon': '100/hour',
             'user': '1000/hour',
             'premium': '5000/hour',
             'admin': '10000/hour'
         }
     }

     # Custom throttle class
     from rest_framework.throttling import UserRateThrottle

     class PremiumUserRateThrottle(UserRateThrottle):
         scope = 'premium'

         def allow_request(self, request, view):
             if request.user.is_authenticated and hasattr(request.user, 'is_premium'):
                 if request.user.is_premium:
                     return super().allow_request(request, view)
             return True  # No throttling for non-premium users

     # Usage in views
     class APIViewWithThrottling(viewsets.ModelViewSet):
         throttle_classes = [PremiumUserRateThrottle]
         throttle_scope = 'premium'
     ```

---

## Database Optimization

7. **Query Optimization Patterns**
   - Use select_related for foreign key relationships
   - Use prefetch_related for many-to-many and reverse foreign keys
   - Implement database indexing strategies
   - Example optimization techniques:
     ```python
     from django.db import models
     from rest_framework import viewsets

     class OptimizedUserViewSet(viewsets.ModelViewSet):
         def get_queryset(self):
             queryset = User.objects.all()

             if self.action == 'list':
                 # Optimize for list view
                 queryset = queryset.select_related('profile').prefetch_related(
                     'groups__permissions',
                     'user_permissions'
                 ).only(
                     'id', 'username', 'email', 'first_name', 'last_name',
                     'profile__bio', 'profile__avatar'
                 )
             elif self.action == 'retrieve':
                 # Optimize for detail view
                 queryset = queryset.select_related('profile').prefetch_related(
                     'posts__comments__author',
                     'groups',
                     'followers',
                     'following'
                 )

             return queryset

     # Model with proper indexing
     class Post(models.Model):
         title = models.CharField(max_length=200, db_index=True)
         author = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True)
         created_at = models.DateTimeField(auto_now_add=True, db_index=True)
         status = models.CharField(max_length=20, choices=STATUS_CHOICES, db_index=True)
         tags = models.ManyToManyField('Tag', through='PostTag')

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

     # Custom manager for common queries
     class PostManager(models.Manager):
         def published(self):
             return self.filter(status='published')

         def by_author(self, author):
             return self.filter(author=author)

         def with_tags(self):
             return self.prefetch_related('tags')
     ```

8. **Caching Strategies**
   - Implement view-level caching
   - Use database query caching
   - Cache expensive computations
   - Example caching implementations:
     ```python
     from django.core.cache import cache
     from django.utils.decorators import method_decorator
     from django.views.decorators.cache import cache_page
     from rest_framework.response import Response

     class CachedViewSet(viewsets.ModelViewSet):
         @method_decorator(cache_page(60 * 15))  # Cache for 15 minutes
         def list(self, request, *args, **kwargs):
             return super().list(request, *args, **kwargs)

         def retrieve(self, request, *args, **kwargs):
             instance = self.get_object()
             cache_key = f"user_{instance.id}_detail"

             cached_data = cache.get(cache_key)
             if cached_data:
                 return Response(cached_data)

             serializer = self.get_serializer(instance)
             cache.set(cache_key, serializer.data, 60 * 30)  # Cache for 30 minutes
             return Response(serializer.data)

     # Cache invalidation
     from django.db.models.signals import post_save, post_delete
     from django.dispatch import receiver

     @receiver(post_save, sender=User)
     @receiver(post_delete, sender=User)
     def invalidate_user_cache(sender, instance, **kwargs):
         cache_key = f"user_{instance.id}_detail"
         cache.delete(cache_key)
         # Invalidate related caches
         cache.delete_many([f"user_list_page_{i}" for i in range(1, 10)])
     ```

---

## API Design and Documentation

9. **Versioning Strategy**
   - Implement API versioning
   - Handle backward compatibility
   - Provide clear migration paths
   - Example versioning implementation:
     ```python
     # settings.py
     REST_FRAMEWORK = {
         'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
         'DEFAULT_VERSION': 'v1',
         'ALLOWED_VERSIONS': ['v1', 'v2'],
         'VERSION_PARAM': 'version'
     }

     # urls.py
     from django.urls import path, include

     urlpatterns = [
         path('api/v1/', include('myapp.urls', namespace='v1')),
         path('api/v2/', include('myapp.v2_urls', namespace='v2')),
     ]

     # Versioned serializers
     class UserV1Serializer(serializers.ModelSerializer):
         class Meta:
             model = User
             fields = ['id', 'username', 'email']

     class UserV2Serializer(serializers.ModelSerializer):
         full_name = serializers.SerializerMethodField()

         class Meta:
             model = User
             fields = ['id', 'username', 'email', 'full_name', 'created_at']

         def get_full_name(self, obj):
             return f"{obj.first_name} {obj.last_name}"

     # Versioned views
     class UserViewSet(viewsets.ModelViewSet):
         queryset = User.objects.all()

         def get_serializer_class(self):
             if self.request.version == 'v2':
                 return UserV2Serializer
             return UserV1Serializer
     ```

10. **API Documentation with Swagger/OpenAPI**
    - Auto-generate API documentation
    - Add detailed endpoint descriptions
    - Include example requests and responses
    - Example documentation setup:
      ```python
      # settings.py
      INSTALLED_APPS = [
          'drf_spectacular',
      ]

      REST_FRAMEWORK = {
          'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
      }

      SPECTACULAR_SETTINGS = {
          'TITLE': 'Your API',
          'DESCRIPTION': 'Your project description',
          'VERSION': '1.0.0',
          'SERVE_INCLUDE_SCHEMA': False,
      }

      # Documented views
      from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample
      from drf_spectacular.types import OpenApiTypes

      class DocumentedUserViewSet(viewsets.ModelViewSet):
          @extend_schema(
              summary="List all users",
              description="Retrieve a paginated list of all users in the system.",
              parameters=[
                  OpenApiParameter(
                      name='is_active',
                      type=OpenApiTypes.BOOL,
                      description='Filter by active status'
                  ),
                  OpenApiParameter(
                      name='search',
                      type=OpenApiTypes.STR,
                      description='Search in username, email, and name fields'
                  ),
              ],
              examples=[
                  OpenApiExample(
                      'Active users only',
                      value={'is_active': True},
                      request_only=True,
                  ),
              ]
          )
          def list(self, request, *args, **kwargs):
              return super().list(request, *args, **kwargs)

          @extend_schema(
              summary="Create a new user",
              description="Create a new user account with the provided information.",
              request=UserCreateSerializer,
              responses={201: UserSerializer}
          )
          def create(self, request, *args, **kwargs):
              return super().create(request, *args, **kwargs)
      ```

---

## Testing and Quality Assurance

11. **API Testing Strategies**
    - Write comprehensive test suites
    - Test serializers, views, and permissions
    - Use factories for test data generation
    - Example testing patterns:
      ```python
      from rest_framework.test import APITestCase, APIClient
      from rest_framework import status
      from django.contrib.auth import get_user_model
      from django.urls import reverse
      import factory

      User = get_user_model()

      class UserFactory(factory.django.DjangoModelFactory):
          class Meta:
              model = User

          username = factory.Sequence(lambda n: f"user{n}")
          email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
          first_name = factory.Faker('first_name')
          last_name = factory.Faker('last_name')

      class UserAPITestCase(APITestCase):
          def setUp(self):
              self.client = APIClient()
              self.user = UserFactory()
              self.admin_user = UserFactory(is_staff=True)

          def test_list_users_authenticated(self):
              self.client.force_authenticate(user=self.user)
              url = reverse('user-list')
              response = self.client.get(url)

              self.assertEqual(response.status_code, status.HTTP_200_OK)
              self.assertIn('results', response.data)

          def test_create_user_with_valid_data(self):
              url = reverse('user-list')
              data = {
                  'username': 'newuser',
                  'email': 'newuser@example.com',
                  'password': 'testpass123',
                  'password_confirm': 'testpass123'
              }

              response = self.client.post(url, data)

              self.assertEqual(response.status_code, status.HTTP_201_CREATED)
              self.assertTrue(User.objects.filter(username='newuser').exists())

          def test_permission_denied_for_unauthorized_access(self):
              url = reverse('user-detail', kwargs={'pk': self.user.pk})
              response = self.client.delete(url)

              self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

      # Serializer tests
      class UserSerializerTestCase(TestCase):
          def test_valid_serializer(self):
              data = {
                  'username': 'testuser',
                  'email': 'test@example.com',
                  'password': 'testpass123',
                  'password_confirm': 'testpass123'
              }

              serializer = UserCreateSerializer(data=data)
              self.assertTrue(serializer.is_valid())

              user = serializer.save()
              self.assertEqual(user.username, 'testuser')
              self.assertTrue(user.check_password('testpass123'))

          def test_password_mismatch_validation(self):
              data = {
                  'username': 'testuser',
                  'email': 'test@example.com',
                  'password': 'testpass123',
                  'password_confirm': 'differentpass'
              }

              serializer = UserCreateSerializer(data=data)
              self.assertFalse(serializer.is_valid())
              self.assertIn('non_field_errors', serializer.errors)
      ```

---

## Production Deployment

12. **Security and Production Settings**
    - Configure proper security settings
    - Implement CORS policies
    - Set up proper logging and monitoring
    - Example production configuration:
      ```python
      # settings/production.py
      import os
      from .base import *

      DEBUG = False
      ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')

      # Security settings
      SECURE_BROWSER_XSS_FILTER = True
      SECURE_CONTENT_TYPE_NOSNIFF = True
      SECURE_HSTS_SECONDS = 31536000
      SECURE_HSTS_INCLUDE_SUBDOMAINS = True
      SECURE_HSTS_PRELOAD = True
      SECURE_SSL_REDIRECT = True

      # CORS settings
      CORS_ALLOWED_ORIGINS = [
          "https://yourdomain.com",
          "https://www.yourdomain.com",
      ]

      # Logging configuration
      LOGGING = {
          'version': 1,
          'disable_existing_loggers': False,
          'formatters': {
              'verbose': {
                  'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
                  'style': '{',
              },
          },
          'handlers': {
              'file': {
                  'level': 'INFO',
                  'class': 'logging.FileHandler',
                  'filename': '/var/log/django/debug.log',
                  'formatter': 'verbose',
              },
          },
          'root': {
              'handlers': ['file'],
              'level': 'INFO',
          },
      }

      # Database connection pooling
      DATABASES = {
          'default': {
              'ENGINE': 'django.db.backends.postgresql',
              'NAME': os.environ.get('DB_NAME'),
              'USER': os.environ.get('DB_USER'),
              'PASSWORD': os.environ.get('DB_PASSWORD'),
              'HOST': os.environ.get('DB_HOST'),
              'PORT': os.environ.get('DB_PORT'),
              'OPTIONS': {
                  'MAX_CONNS': 20,
                  'MIN_CONNS': 5,
              }
          }
      }
      ```

---

## Summary Checklist

- [ ] Use ViewSets for standard CRUD operations
- [ ] Implement focused serializers for different operations
- [ ] Add comprehensive validation at field and object levels
- [ ] Configure JWT authentication with refresh tokens
- [ ] Create custom permission classes for complex authorization
- [ ] Implement proper database query optimization
- [ ] Use caching strategies for performance
- [ ] Add API versioning for backward compatibility
- [ ] Generate comprehensive API documentation
- [ ] Write thorough test suites for all components
- [ ] Configure security settings for production
- [ ] Implement proper error handling and logging
- [ ] Use database indexing for query optimization
- [ ] Set up rate limiting and throttling
- [ ] Follow RESTful API design principles

---

Follow these practices to build robust, scalable, and maintainable REST APIs with Django REST Framework.
Django REST Framework - Cursor IDE AI Rule