Skip to content
Back to skills

Drf Best Practices

ASecurity

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.

  • 3 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added August 31, 2026
developmentpythonrustgosqldjangoapibackendsecurity

Works with

  • api

Security analysis

A100/100

Scanned September 21, 2026

npx -y skills add Spardutti/claude-skills --skill drf-best-practices --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Drf Best Practices?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Drf Best Practices
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/spardutti-drf-best-practices/badge)](https://www.skillsdirectory.com/skills/spardutti-drf-best-practices)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: drf-best-practices
tracks: djangorestframework@3.18 (pypi)
metadata:
  gate-paths: "**/*.py"
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

```python
# 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

```python
# 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

```python
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

```python
# 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

```python
# 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

```python
# 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

```python
# 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

```python
class IsOwner(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.user == request.user
```

### Global Defaults in `settings.py`

```python
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

```python
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

1. **Thin serializers** — validation and serialization only, no business logic
2. **Separate serializers** for list, detail, and create/update operations
3. **Never `fields = "__all__"`** — explicitly list every field
4. **Optimize every queryset** — `select_related`, `prefetch_related`, `defer`
5. **Service layer** for business logic — keep views and serializers declarative
6. **Scope querysets to user** — never expose unfiltered `.objects.all()`
7. **Object-level permissions** — implement `has_object_permission` for detail views
8. **Set global defaults** — authentication, permissions, throttling, pagination
9. **Use `transaction.atomic`** — wrap multi-step writes in services
10. **Filter in SQL** — never filter querysets in Python

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.