| name | django-rest-framework |
| description | [Applies to: **/*.py] Enforces modern, secure, and performant `django-rest-framework` best practices for API development, focusing on maintainability and scalability. |
| source | cursor_mdc |
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 into settings.py.
urls.py with DefaultRouter: Automate URL patterns for ViewSets.
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'
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',
}
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',
'api',
]
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)),
]
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')),
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
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()]
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 :
model = Review
fields = [, , , , ]
read_only_fields = [, ]
(serializers.ModelSerializer):
category: CategorySerializer = CategorySerializer(read_only=)
reviews: ReviewSerializer = ReviewSerializer(many=, read_only=)
:
model = Product
fields = [, , , , , ]
read_only_fields = []
() -> Product:
validated_data[] = .context[].user
().create(validated_data)
(serializers.Serializer):
username: = serializers.CharField(max_length=)
email: = serializers.EmailField()
password: = serializers.CharField(write_only=)
password_confirm: = serializers.CharField(write_only=)
() -> [, ]:
data[] != data[]:
serializers.ValidationError({: })
data
() -> User:
user = User.objects.create_user(
username=validated_data[],
email=validated_data[],
password=validated_data[]
)
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
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]
def get_queryset(self) -> QuerySet[Product]:
if self.action == 'list':
return self.queryset.(is_active=)
.queryset
() -> :
serializer.save(owner=.request.user)
() -> Response:
product = .get_object()
serializer = ReviewSerializer(data=request.data)
serializer.is_valid(raise_exception=)
serializer.save(product=product, user=request.user)
Response(serializer.data, status=status.HTTP_201_CREATED)
(viewsets.ReadOnlyModelViewSet):
queryset: QuerySet[Category] = Category.objects.()
serializer_class: [CategorySerializer] = CategorySerializer
permission_classes: [[IsAuthenticated]] = [IsAuthenticated]
(viewsets.ViewSet):
permission_classes: [[IsAuthenticated]] = [IsAuthenticated]
() -> Response:
serializer = UserRegistrationSerializer(data=request.data)
serializer.is_valid(raise_exception=)
user = serializer.save()
Response({: }, status=status.HTTP_201_CREATED)
() -> Response:
serializer = ProductSerializer(request.user)
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
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
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.
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']
6. Performance Considerations
Prevent N+1 queries by using select_related and prefetch_related in your get_queryset method.
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_queryset(self) -> QuerySet[Product]:
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.
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:
response = exception_handler(exc, context)
if response is not None:
if response.status_code == status.HTTP_400_BAD_REQUEST and 'detail' not in response.data:
response.data = {'errors': response.data}
return response
return Response(
{'detail': 'An unexpected error occurred.'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
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.
9. Rate Limiting
Implement DRF's throttling classes to protect your API from abuse.
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',
}
}
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
from typing import Any
class AsyncDataView(APIView):
async def get(self, request: Any, *args: Any, **kwargs: Any) -> Response:
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})