django-rest-framework Best Practices
This guide outlines our definitive standards for building APIs with Django REST Framework (DRF). Adhere to these rules to ensure consistency, security, and performance across all projects.
1. Code Organization and Structure
Always structure your API within a dedicated Django app, typically named api. Centralize DRF-specific settings and use DefaultRouter for URL management.
- Dedicated
api/app: Isolate API logic. api_settings.py: Store all DRF global settings here, then import intosettings.py.urls.pywithDefaultRouter: Automate URL patterns for ViewSets.
# project_root/api/apps.py
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'
# project_root/api/settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 25,
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', # For OpenAPI
}
# project_root/settings.py
from .api.settings import REST_FRAMEWORK as API_REST_FRAMEWORK
REST_FRAMEWORK = API_REST_FRAMEWORK
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework_simplejwt',
'django_filters',
'drf_spectacular', # For OpenAPI
'api',
]
# project_root/api/urls.py
from rest_framework.routers import DefaultRouter
from .views import UserViewSet, ProductViewSet
from django.urls import path, include
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
router.register(r'products', ProductViewSet, basename='product')
urlpatterns = [
path('', include(router.urls)),
]
# project_root/project_name/urls.py
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('api.urls')), # Versioned API namespace
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
2. Serializers: Efficiency and Validation
Prioritize ModelSerializer for CRUD operations. Use custom Serializer classes only for complex, non-model-backed data or advanced validation. Always use select_related/prefetch_related to prevent N+1 queries.
from rest_framework import serializers
from django.db.models import QuerySet
from typing import Any
from .models import Product, Category, Review, User
# ❌ BAD: N+1 queries for related objects, exposing all fields
class BadProductSerializer(serializers.ModelSerializer):
category = serializers.CharField(source='category.name')
reviews = serializers.SerializerMethodField()
class Meta:
model = Product
fields = ['id', 'name', 'price', 'category', 'reviews']
def get_reviews(self, obj: Product) -> list[dict[str, Any]]:
return [{'text': r.text, 'rating': r.rating} for r in obj.reviews.all()]
# ✅ GOOD: Optimized queries, controlled field exposure, explicit types
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name']
class ReviewSerializer(serializers.ModelSerializer):
user = serializers.SlugRelatedField(slug_field='username', read_only=True)
class Meta:
model = Review
fields = ['id', 'user', 'text', 'rating', 'created_at']
read_only_fields = ['user', 'created_at']
class ProductSerializer(serializers.ModelSerializer):
category: CategorySerializer = CategorySerializer(read_only=True) # Type hint for nested serializer
reviews: ReviewSerializer = ReviewSerializer(many=True, read_only=True) # Type hint for nested list
class Meta:
model = Product
fields = ['id', 'name', 'price', 'category', 'reviews', 'stock_count']
read_only_fields = ['stock_count'] # Fields that should not be updated via API
def create(self, validated_data: dict[str, Any]) -> Product:
# Custom logic if needed, e.g., setting owner
validated_data['owner'] = self.context['request'].user
return super().create(validated_data)
# ✅ GOOD: Custom serializer for non-model data or specific input validation
class UserRegistrationSerializer(serializers.Serializer):
username: str = serializers.CharField(max_length=150)
email: str = serializers.EmailField()
password: str = serializers.CharField(write_only=True)
password_confirm: str = serializers.CharField(write_only=True)
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
if data['password'] != data['password_confirm']:
raise serializers.ValidationError({"password_confirm": "Passwords do not match."})
return data
def create(self, validated_data: dict[str, Any]) -> User:
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password']
)
return user
3. ViewSets & Routers: Streamlining Endpoints
Always use ModelViewSet for standard CRUD operations. For read-only resources, use ReadOnlyModelViewSet. Leverage DefaultRouter to automatically generate URLs.
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from django.db.models import QuerySet
from typing import Any
from .models import Product, Category, User
from .serializers import ProductSerializer, CategorySerializer, UserRegistrationSerializer, ReviewSerializer
# ❌ BAD: Manual URL routing, repetitive logic for each HTTP method
# class ProductList(APIView):
# def get(self, request): ...
# def post(self, request): ...
# ✅ GOOD: ModelViewSet for full CRUD, optimized queryset, explicit permissions
class ProductViewSet(viewsets.ModelViewSet):
queryset: QuerySet[Product] = Product.objects.select_related('category').prefetch_related('reviews__user').all()
serializer_class: type[ProductSerializer] = ProductSerializer
permission_classes: list[type[IsAuthenticated | IsAdminUser]] = [IsAuthenticated, IsAdminUser] # Example: Admin can manage products
def get_queryset(self) -> QuerySet[Product]:
# Optimize queryset for specific actions if needed
if self.action == 'list':
return self.queryset.filter(is_active=True)
return self.queryset
def perform_create(self, serializer: ProductSerializer) -> None:
serializer.save(owner=self.request.user) # Automatically assign owner
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def add_review(self, request: Any, pk: int | None = None) -> Response:
product = self.get_object()
serializer = ReviewSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(product=product, user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
# ✅ GOOD: ReadOnlyModelViewSet for read-only resources
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
queryset: QuerySet[Category] = Category.objects.all()
serializer_class: type[CategorySerializer] = CategorySerializer
permission_classes: list[type[IsAuthenticated]] = [IsAuthenticated]
# ✅ GOOD: Custom ViewSet for non-model operations (e.g., user registration)
class UserViewSet(viewsets.ViewSet):
permission_classes: list[type[IsAuthenticated]] = [IsAuthenticated] # Adjust as needed for registration
@action(detail=False, methods=['post'], permission_classes=[]) # No auth for registration
def register(self, request: Any) -> Response:
serializer = UserRegistrationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
return Response({"message": "User registered successfully."}, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['get'])
def me(self, request: Any) -> Response:
serializer = ProductSerializer(request.user) # Example: reuse ProductSerializer for user details
return Response(serializer.data)
4. Authentication & Permissions: Secure Access
Always use token-based authentication (JWT or DRF's TokenAuthentication) for stateless APIs. Implement granular permissions using DRF's built-in classes or custom ones. Never use AllowAny unless the endpoint is truly public.
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.request import Request
from typing import Any
# ✅ GOOD: Custom object-level permission
class IsOwnerOrReadOnly(BasePermission):
"""
Object-level permission to allow only owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
if request.method in SAFE_METHODS:
return True
return obj.owner == request.user
# Apply to ViewSet:
# class ProductViewSet(viewsets.ModelViewSet):
# permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
5. Pagination, Filtering & Versioning
Enable PageNumberPagination (default) or CursorPagination for large datasets. Use django-filter for query parameter filtering. Implement URL versioning (/api/v1/) for clear API evolution.
# project_root/api/filters.py
import django_filters
from .models import Product
class ProductFilter(django_filters.FilterSet):
min_price = django_filters.NumberFilter(field_name="price", lookup_expr='gte')
max_price = django_filters.NumberFilter(field_name="price", lookup_expr='lte')
category_name = django_filters.CharFilter(field_name="category__name", lookup_expr='icontains')
class Meta:
model = Product
fields = ['category', 'is_active']
# Apply to ViewSet:
# class ProductViewSet(viewsets.ModelViewSet):
# filterset_class = ProductFilter
6. Performance Considerations
Prevent N+1 queries by using select_related and prefetch_related in your get_queryset method.
# ✅ GOOD: Optimized queryset in ViewSet
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_queryset(self) -> QuerySet[Product]:
# Always optimize for common list/retrieve actions
return self.queryset.select_related('category').prefetch_related('reviews__user')
7. Error Handling
Leverage DRF's built-in exception handling. For custom errors, define a custom exception handler that returns consistent, machine-readable responses.
# project_root/api/utils.py
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status
from typing import Any
def custom_exception_handler(exc: Exception, context: dict[str, Any]) -> Response | None:
# Call DRF's default exception handler first
response = exception_handler(exc, context)
if response is not None:
# Example: Customize validation error messages
if response.status_code == status.HTTP_400_BAD_REQUEST and 'detail' not in response.data:
response.data = {'errors': response.data}
return response
# Handle unhandled exceptions gracefully
return Response(
{'detail': 'An unexpected error occurred.'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# project_root/api/settings.py
REST_FRAMEWORK = {
# ...
'EXCEPTION_HANDLER': 'api.utils.custom_exception_handler',
}
8. Type Hints
Mandate type hints for all DRF components: serializers, views, viewsets, and custom permissions. This improves code readability, maintainability, and enables static analysis.
# All examples above demonstrate mandatory type hints.
# Example:
# class ProductSerializer(serializers.ModelSerializer):
# category: CategorySerializer = CategorySerializer(read_only=True)
# def create(self, validated_data: dict[str, Any]) -> Product: ...
9. Rate Limiting
Implement DRF's throttling classes to protect your API from abuse.
# project_root/api/settings.py
REST_FRAMEWORK = {
# ...
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
'burst': '60/min', # Custom throttle rate
}
}
# Apply to ViewSet for custom throttling:
# class ProductViewSet(viewsets.ModelViewSet):
# throttle_classes = [UserRateThrottle, AnonRateThrottle] # Or custom throttle
# throttle_scope = 'burst' # Use the 'burst' rate defined in settings
10. Async Support
For high-throughput endpoints, isolate async code within async def views or specific ViewSet actions. Run your project with an ASGI server (Uvicorn/Daphne). DRF's native async support is evolving; consider adrf for full async ViewSets if truly necessary, but be aware it's still experimental.
from rest_framework.views import APIView
from rest_framework.response import Response
import asyncio
import httpx # For async HTTP requests
from typing import Any
# ✅ GOOD: Isolate async logic to specific views/actions
class AsyncDataView(APIView):
async def get(self, request: Any, *args: Any, **kwargs: Any) -> Response:
# Simulate an async operation, e.g., calling an external async service
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/external-data")
response.raise_for_status()
data = response.json()
return Response({"message": "Data fetched asynchronously", "data": data})
# project_root/api/urls.py
# from .views import AsyncDataView
# urlpatterns = [
# path('async-data/', AsyncDataView.as_view()),
# # ...
# ]