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
# BAD: business logic in serializer
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
# GOOD: serializer validates, service handles logic
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
# BAD: one serializer doing everything
class ArticleSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
author_id = serializers.IntegerField(write_only=True)
class Meta:
model = Article
fields = "__all__"
# GOOD: dedicated serializers per operation
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
# BAD: N+1 queries — each order triggers separate queries for user and items
def get_queryset(self):
return Order.objects.all()
# GOOD: eager-load relationships
def get_queryset(self):
return (
Order.objects
.select_related("user", "shipping_address") # ForeignKey / OneToOne
.prefetch_related("items", "items__product") # ManyToMany / reverse FK
)
Use defer() for Large Fields and Filter in SQL
# List view doesn't need the full body
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")
# BAD: filtering in Python
def get_queryset(self):
return [o for o in Order.objects.all() if o.is_active]
# GOOD: filter in SQL
def get_queryset(self):
return Order.objects.filter(is_active=True)
Service Layer
Keep Business Logic Out of Views and Serializers
# services/order_service.py
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()
# View calls service, not ORM directly
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
# BAD: any authenticated user sees all orders (IDOR vulnerability)
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.all()
# GOOD: scope to current user
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_permissionfor 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