Claude-initial-setup drf-api-patterns
install
source · Clone the upstream repo
git clone https://github.com/VersoXBT/claude-initial-setup
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/VersoXBT/claude-initial-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/django/drf-api-patterns" ~/.claude/skills/versoxbt-claude-initial-setup-drf-api-patterns && rm -rf "$T"
manifest:
skills/django/drf-api-patterns/SKILL.mdsource content
DRF API Patterns
Build production-grade REST APIs with Django REST Framework. DRF provides serialization, authentication, permissions, pagination, and more out of the box, so you write less boilerplate and get consistent API behavior.
When to Use
- User builds REST API endpoints in Django
- User needs serialization, validation, or pagination
- User implements authentication and permissions
- User asks about ViewSets, routers, or DRF best practices
- User needs filtering, search, or ordering on list endpoints
Core Patterns
Serializers
Serializers handle validation and conversion between Python objects and JSON.
from rest_framework import serializers class ArticleSerializer(serializers.ModelSerializer): author_name = serializers.CharField(source="author.get_full_name", read_only=True) comment_count = serializers.IntegerField(read_only=True) tags = serializers.SlugRelatedField( many=True, slug_field="name", queryset=Tag.objects.all() ) class Meta: model = Article fields = [ "id", "title", "slug", "body", "status", "author", "author_name", "tags", "comment_count", "created_at", "updated_at", ] read_only_fields = ["id", "slug", "created_at", "updated_at"] def validate_title(self, value: str) -> str: if len(value) < 5: raise serializers.ValidationError("Title must be at least 5 characters") return value def validate(self, attrs: dict) -> dict: """Cross-field validation.""" if attrs.get("status") == "published" and not attrs.get("body"): raise serializers.ValidationError( {"body": "Published articles must have a body"} ) return attrs # Separate serializers for create vs read class ArticleCreateSerializer(serializers.ModelSerializer): class Meta: model = Article fields = ["title", "body", "tags", "status"] def create(self, validated_data): tags = validated_data.pop("tags", []) article = Article.objects.create( author=self.context["request"].user, **validated_data, ) article.tags.set(tags) return article
ViewSets and Routers
ViewSets combine list, create, retrieve, update, and destroy into one class.
from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from django.db.models import Count class ArticleViewSet(viewsets.ModelViewSet): serializer_class = ArticleSerializer lookup_field = "slug" def get_queryset(self): return ( Article.objects .select_related("author") .prefetch_related("tags") .annotate(comment_count=Count("comments")) .order_by("-created_at") ) def get_serializer_class(self): if self.action == "create": return ArticleCreateSerializer return ArticleSerializer @action(detail=True, methods=["post"]) def publish(self, request, slug=None): article = self.get_object() article.status = "published" article.save(update_fields=["status"]) return Response({"status": "published"}) @action(detail=False, methods=["get"]) def trending(self, request): qs = self.get_queryset().filter( status="published" ).order_by("-view_count")[:10] serializer = self.get_serializer(qs, many=True) return Response(serializer.data) # urls.py from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register("articles", ArticleViewSet, basename="article") urlpatterns = [ path("api/v1/", include(router.urls)), ]
Permissions
from rest_framework.permissions import BasePermission, IsAuthenticated, SAFE_METHODS class IsAuthorOrReadOnly(BasePermission): """Allow authors to edit, everyone else can only read.""" def has_object_permission(self, request, view, obj): if request.method in SAFE_METHODS: return True return obj.author == request.user class IsAdminOrReadOnly(BasePermission): def has_permission(self, request, view): if request.method in SAFE_METHODS: return True return request.user and request.user.is_staff class ArticleViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated, IsAuthorOrReadOnly] def get_permissions(self): if self.action == "list": return [] # Public access for listing if self.action == "publish": return [IsAuthenticated(), IsAdminOrReadOnly()] return super().get_permissions()
Pagination
from rest_framework.pagination import CursorPagination, PageNumberPagination class StandardPagination(PageNumberPagination): page_size = 25 page_size_query_param = "page_size" max_page_size = 100 # Cursor pagination -- efficient for large datasets, no count query class TimelinePagination(CursorPagination): page_size = 50 ordering = "-created_at" cursor_query_param = "cursor" class ArticleViewSet(viewsets.ModelViewSet): pagination_class = StandardPagination # Global default in settings.py REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "myapp.pagination.StandardPagination", "PAGE_SIZE": 25, }
Filtering with django-filter
import django_filters from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter, OrderingFilter class ArticleFilter(django_filters.FilterSet): created_after = django_filters.DateFilter( field_name="created_at", lookup_expr="gte" ) created_before = django_filters.DateFilter( field_name="created_at", lookup_expr="lte" ) min_views = django_filters.NumberFilter( field_name="view_count", lookup_expr="gte" ) tags = django_filters.CharFilter(method="filter_by_tags") class Meta: model = Article fields = ["status", "category", "author"] def filter_by_tags(self, queryset, name, value): tag_names = value.split(",") return queryset.filter(tags__name__in=tag_names).distinct() class ArticleViewSet(viewsets.ModelViewSet): filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = ArticleFilter search_fields = ["title", "body", "author__username"] ordering_fields = ["created_at", "view_count", "title"] ordering = ["-created_at"]
Throttling
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle class BurstRateThrottle(UserRateThrottle): scope = "burst" class SustainedRateThrottle(UserRateThrottle): scope = "sustained" # settings.py REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "myapp.throttles.BurstRateThrottle", "myapp.throttles.SustainedRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "burst": "60/min", "sustained": "1000/day", "anon": "20/min", }, }
API Versioning
# settings.py REST_FRAMEWORK = { "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning", "ALLOWED_VERSIONS": ["v1", "v2"], "DEFAULT_VERSION": "v1", } # urls.py urlpatterns = [ path("api/<version>/", include(router.urls)), ] # Conditional logic in views class ArticleViewSet(viewsets.ModelViewSet): def get_serializer_class(self): if self.request.version == "v2": return ArticleV2Serializer return ArticleSerializer
Anti-Patterns
- Fat serializers with business logic: Serializers should validate and transform. Keep business logic in services or model methods.
- Using
for non-CRUD endpoints: If you only need list and retrieve, useModelViewSet
andmixins.ListModelMixin
withmixins.RetrieveModelMixin
.GenericViewSet - No pagination on list endpoints: Unbounded QuerySets will crash on large tables. Always set a pagination class.
- Skipping select_related in get_queryset: DRF serializers accessing related fields trigger N+1 queries. Always optimize the QuerySet.
- Testing with the Django test client instead of DRF's: Use
andAPIClient
for proper content negotiation and authentication.APIRequestFactory
Quick Reference
| Feature | Configuration |
|---|---|
| Serializer | with |
| ViewSet | + Router |
| Custom action | |
| Permission | |
| Pagination | / |
| Filtering | + |
| Search | + |
| Ordering | + |
| Throttling | + |
| Versioning | + |