| name | drf-best-practices |
| category | Backend |
| description | MUST USE when creating or editing Django REST Framework views, serializers, viewsets, or API configuration. Enforces thin serializers, service layer, queryset optimization, and object-level permissions. |
Django REST Framework Best Practices
Keep serializers thin, views declarative, business logic in services, and always optimize querysets.
Architecture Rules
| Layer | Responsibility | Anti-Pattern |
|---|
| Serializer | Validation + serialization only | Business logic in .create()/.validate() |
| View/ViewSet | HTTP concerns + orchestration | Fat views with inline queries and logic |
| Service | Business logic + state changes | Skipping services, putting logic in serializers |
| Model | Data integrity + simple properties | God models with 50+ methods |
Serializer Rules
Thin Serializers — Validation Only
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ["id", "user", "items", "total"]
def create(self, validated_data):
items_data = validated_data.pop("items")
order = Order.objects.create(**validated_data)
for item in items_data:
if item["product"].stock < item["quantity"]:
raise serializers.ValidationError("Out of stock")
item["product"].stock -= item["quantity"]
item["product"].save()
OrderItem.objects.create(order=order, **item)
send_confirmation_email(order)
return order
class OrderCreateSerializer(serializers.Serializer):
items = OrderItemSerializer(many=True)
shipping_address = serializers.CharField(max_length=500)
def validate_items(self, value):
if not value:
raise serializers.ValidationError("At least one item required.")
return value
Separate Serializers for Read vs Write
class ArticleSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
author_id = serializers.IntegerField(write_only=True)
class Meta:
model = Article
fields = "__all__"
class ArticleListSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.get_full_name")
class Meta:
model = Article
fields = ["id", "title", "author_name", "created_at"]
class ArticleDetailSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Article
fields = ["id", "title", "body", "author", "tags", "created_at"]
class ArticleCreateSerializer(serializers.Serializer):
title = serializers.CharField(max_length=200)
body = serializers.CharField()
tag_ids = serializers.ListField(child=serializers.IntegerField())
Never Use fields = "__all__" — explicitly list every field. __all__ leaks new model fields silently.
ViewSet Rules
Action-Specific Serializers and Permissions
class ArticleViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == "list":
return ArticleListSerializer
if self.action == "retrieve":
return ArticleDetailSerializer
return ArticleCreateSerializer
def get_permissions(self):
if self.action in ["list", "retrieve"]:
return [IsAuthenticated()]
if self.action == "destroy":
return [IsAdminUser()]
return [IsAuthenticated(), IsProjectOwner()]
def get_queryset(self):
return Article.objects.select_related("author").prefetch_related("tags")
def perform_create(self, serializer):
article_service.create_article(
data=serializer.validated_data,
user=self.request.user,
)
Queryset Optimization
Always Optimize Related Lookups
def get_queryset(self):
return Order.objects.all()
def get_queryset(self):
return (
Order.objects
.select_related("user", "shipping_address")
.prefetch_related("items", "items__product")
)
Use defer() for Large Fields and Filter in SQL
def get_queryset(self):
if self.action == "list":
return Article.objects.defer("body").select_related("author")
return Article.objects.select_related("author").prefetch_related("tags")
def get_queryset(self):
return [o for o in Order.objects.all() if o.is_active]
def get_queryset(self):
return Order.objects.filter(is_active=True)
Service Layer
Keep Business Logic Out of Views and Serializers
from django.db import transaction
class OrderService:
@transaction.atomic
def create_order(self, data: dict, user) -> Order:
order = Order.objects.create(user=user, shipping_address=data["shipping_address"])
for item_data in data["items"]:
product = Product.objects.select_for_update().get(id=item_data["product_id"])
if product.stock < item_data["quantity"]:
raise InsufficientStockError(product)
product.stock -= item_data["quantity"]
product.save(update_fields=["stock"])
OrderItem.objects.create(order=order, **item_data)
return order
order_service = OrderService()
class OrderViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
serializer_class = OrderCreateSerializer
permission_classes = [IsAuthenticated]
def perform_create(self, serializer):
order_service.create_order(
data=serializer.validated_data,
user=self.request.user,
)
Security
Scope Querysets to the User
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.all()
class OrderViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Order.objects.filter(user=self.request.user)
Object-Level Permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.user == request.user
Global Defaults in settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {"anon": "100/hour", "user": "1000/hour"},
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
"DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"],
}
Validation
class EventSerializer(serializers.Serializer):
title = serializers.CharField(max_length=200)
start = serializers.DateTimeField()
end = serializers.DateTimeField()
def validate_title(self, value):
if Event.objects.filter(title__iexact=value).exists():
raise serializers.ValidationError("Event with this title already exists.")
return value
def validate(self, data):
if data["start"] >= data["end"]:
raise serializers.ValidationError("End must be after start.")
return data
Always validate API input through serializers — never trust request.data directly.
Rules Summary
- Thin serializers — validation and serialization only, no business logic
- Separate serializers for list, detail, and create/update operations
- Never
fields = "__all__" — explicitly list every field
- Optimize every queryset —
select_related, prefetch_related, defer
- Service layer for business logic — keep views and serializers declarative
- Scope querysets to user — never expose unfiltered
.objects.all()
- Object-level permissions — implement
has_object_permission for detail views
- Set global defaults — authentication, permissions, throttling, pagination
- Use
transaction.atomic — wrap multi-step writes in services
- Filter in SQL — never filter querysets in Python