Claude-skill-registry django-drf
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/django-drf-majiayu000-claude-skill-registr" ~/.claude/skills/majiayu000-claude-skill-registry-django-drf && rm -rf "$T"
manifest:
skills/data/django-drf-majiayu000-claude-skill-registr/SKILL.mdsource content
Critical Patterns
- ALWAYS separate serializers by operation: Read / Create / Update / Include
- ALWAYS use
for complex filtering (notfilterset_class
)filterset_fields - ALWAYS validate unknown fields in write serializers (inherit
)BaseWriteSerializer - ALWAYS use
/select_related
inprefetch_related
to avoid N+1get_queryset() - ALWAYS handle
inswagger_fake_view
for schema generationget_queryset() - ALWAYS use
for OpenAPI docs on@extend_schema_fieldSerializerMethodField - NEVER put business logic in serializers - use services/utils
- NEVER use auto-increment PKs - use UUIDv4 or UUIDv7
- NEVER use trailing slashes in URLs (
)trailing_slash=False
Note:
is specific to drf-spectacular for OpenAPI schema generation.swagger_fake_view
Implementation Checklist
When implementing a new endpoint, review these patterns in order:
| # | Pattern | Reference | Key Points |
|---|---|---|---|
| 1 | Models | | UUID PK, /, |
| 2 | ViewSets | , | Inherit , with N+1 prevention |
| 3 | Serializers | | Separate Read/Create/Update/Include, inherit |
| 4 | Filters | | Use , inherit base filter classes |
| 5 | Permissions | | , |
| 6 | Pagination | | Custom pagination class if needed |
| 7 | URL Routing | | , kebab-case paths |
| 8 | OpenAPI Schema | | with drf-spectacular |
| 9 | Tests | | JSON:API content type, fixture patterns |
Full file paths: See references/file-locations.md
Decision Trees
Which Serializer?
GET list/retrieve → <Model>Serializer POST create → <Model>CreateSerializer PATCH update → <Model>UpdateSerializer ?include=... → <Model>IncludeSerializer
Which Base Serializer?
Read-only serializer → BaseModelSerializerV1 Create with tenant_id → RLSSerializer + BaseWriteSerializer (auto-injects tenant_id on create) Update with validation → BaseWriteSerializer (tenant_id already exists on object) Non-model data → BaseSerializerV1
Which Filter Base?
Direct FK to Provider → BaseProviderFilter FK via Scan → BaseScanProviderFilter No provider relation → FilterSet
Which Base ViewSet?
RLS-protected model → BaseRLSViewSet (most common) Tenant operations → BaseTenantViewset User operations → BaseUserViewset No RLS required → BaseViewSet (rare)
Resource Name Format?
Single word model → plural lowercase (Provider → providers) Multi-word model → plural lowercase kebab (ProviderGroup → provider-groups) Through/join model → parent-child pattern (UserRoleRelationship → user-roles) Aggregation/overview → descriptive kebab plural (ComplianceOverview → compliance-overviews)
Serializer Patterns
Base Class Hierarchy
# Read serializer (most common) class ProviderSerializer(RLSSerializer): class Meta: model = Provider fields = ["id", "provider", "uid", "alias", "connected", "inserted_at"] # Write serializer (validates unknown fields) class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer): class Meta: model = Provider fields = ["provider", "uid", "alias"] # Include serializer (sparse fields for ?include=) class ProviderIncludeSerializer(RLSSerializer): class Meta: model = Provider fields = ["id", "alias"] # Minimal fields
SerializerMethodField with OpenAPI
from drf_spectacular.utils import extend_schema_field class ProviderSerializer(RLSSerializer): connection = serializers.SerializerMethodField(read_only=True) @extend_schema_field({ "type": "object", "properties": { "connected": {"type": "boolean"}, "last_checked_at": {"type": "string", "format": "date-time"}, }, }) def get_connection(self, obj): return { "connected": obj.connected, "last_checked_at": obj.connection_last_checked_at, }
Included Serializers (JSON:API)
class ScanSerializer(RLSSerializer): included_serializers = { "provider": "api.v1.serializers.ProviderIncludeSerializer", }
Sensitive Data Masking
def to_representation(self, instance): data = super().to_representation(instance) # Mask by default, expose only on explicit request fields_param = self.context.get("request").query_params.get("fields[my-model]", "") if "api_key" in fields_param: data["api_key"] = instance.api_key_decoded else: data["api_key"] = "****" if instance.api_key else None return data
ViewSet Patterns
get_queryset() with N+1 Prevention
Always combine
swagger_fake_view check with select_related/prefetch_related:
def get_queryset(self): # REQUIRED: Return empty queryset for OpenAPI schema generation if getattr(self, "swagger_fake_view", False): return Provider.objects.none() # N+1 prevention: eager load relationships return Provider.objects.select_related( "tenant", ).prefetch_related( "provider_groups", Prefetch("tags", queryset=ProviderTag.objects.filter(tenant_id=self.request.tenant_id)), )
Why swagger_fake_view? drf-spectacular introspects ViewSets to generate OpenAPI schemas. Without this check, it executes real queries and can fail without request context.
Action-Specific Serializers
def get_serializer_class(self): if self.action == "create": return ProviderCreateSerializer elif self.action == "partial_update": return ProviderUpdateSerializer elif self.action in ["connection", "destroy"]: return TaskSerializer return ProviderSerializer
Dynamic Permissions per Action
class ProviderViewSet(BaseRLSViewSet): required_permissions = [Permissions.MANAGE_PROVIDERS] def set_required_permissions(self): if self.action in ["list", "retrieve"]: self.required_permissions = [] # Read-only = no permission else: self.required_permissions = [Permissions.MANAGE_PROVIDERS]
Cache Decorator
from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control CACHE_DECORATOR = cache_control( max_age=django_settings.CACHE_MAX_AGE, stale_while_revalidate=django_settings.CACHE_STALE_WHILE_REVALIDATE, ) @method_decorator(CACHE_DECORATOR, name="list") @method_decorator(CACHE_DECORATOR, name="retrieve") class ProviderViewSet(BaseRLSViewSet): pass
Custom Actions
# Detail action (operates on single object) @action(detail=True, methods=["post"], url_name="connection") def connection(self, request, pk=None): instance = self.get_object() # Process instance... # List action (operates on collection) @action(detail=False, methods=["get"], url_name="metadata") def metadata(self, request): queryset = self.filter_queryset(self.get_queryset()) # Aggregate over queryset...
Filter Patterns
Base Filter Classes
class BaseProviderFilter(FilterSet): """For models with direct FK to Provider""" provider_id = UUIDFilter(field_name="provider__id", lookup_expr="exact") provider_id__in = UUIDInFilter(field_name="provider__id", lookup_expr="in") provider_type = ChoiceFilter(field_name="provider__provider", choices=Provider.ProviderChoices.choices) class BaseScanProviderFilter(FilterSet): """For models with FK to Scan (Scan has FK to Provider)""" provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
Custom Multi-Value Filters
class UUIDInFilter(BaseInFilter, UUIDFilter): pass class CharInFilter(BaseInFilter, CharFilter): pass class ChoiceInFilter(BaseInFilter, ChoiceFilter): pass
ArrayField Filtering
# Single value contains region = CharFilter(method="filter_region") def filter_region(self, queryset, name, value): return queryset.filter(resource_regions__contains=[value]) # Multi-value overlap region__in = CharInFilter(field_name="resource_regions", lookup_expr="overlap")
Date Range Validation
def filter_queryset(self, queryset): # Require date filter for performance if not (date_filters_provided): raise ValidationError([{ "detail": "At least one date filter is required", "status": 400, "source": {"pointer": "/data/attributes/inserted_at"}, "code": "required", }]) # Validate max range if date_range > settings.FINDINGS_MAX_DAYS_IN_RANGE: raise ValidationError(...) return super().filter_queryset(queryset)
Dynamic FilterSet Selection
def get_filterset_class(self): if self.action in ["latest", "metadata_latest"]: return LatestFindingFilter return FindingFilter
Enum Field Override
class Meta: model = Finding filter_overrides = { FindingDeltaEnumField: {"filter_class": CharFilter}, StatusEnumField: {"filter_class": CharFilter}, SeverityEnumField: {"filter_class": CharFilter}, }
Performance Patterns
PaginateByPkMixin
For large querysets with expensive joins:
class PaginateByPkMixin: def paginate_by_pk(self, request, base_queryset, manager, select_related=None, prefetch_related=None): # 1. Get PKs only (cheap) pk_list = base_queryset.values_list("id", flat=True) page = self.paginate_queryset(pk_list) # 2. Fetch full objects for just the page queryset = manager.filter(id__in=page) if select_related: queryset = queryset.select_related(*select_related) if prefetch_related: queryset = queryset.prefetch_related(*prefetch_related) # 3. Re-sort to preserve DB ordering queryset = sorted(queryset, key=lambda obj: page.index(obj.id)) return self.get_paginated_response(self.get_serializer(queryset, many=True).data)
Prefetch in Serializers
def get_tags(self, obj): # Use prefetched tags if available if hasattr(obj, "prefetched_tags"): return {tag.key: tag.value for tag in obj.prefetched_tags} # Fallback (causes N+1 if not prefetched) return obj.get_tags(self.context.get("tenant_id"))
Naming Conventions
| Entity | Pattern | Example |
|---|---|---|
| Serializer (read) | | |
| Serializer (create) | | |
| Serializer (update) | | |
| Serializer (include) | | |
| Filter | | |
| ViewSet | | |
OpenAPI Documentation
from drf_spectacular.utils import extend_schema, extend_schema_view @extend_schema_view( list=extend_schema(tags=["Provider"], summary="List all providers"), retrieve=extend_schema(tags=["Provider"], summary="Retrieve provider"), create=extend_schema(tags=["Provider"], summary="Create provider"), ) @extend_schema(tags=["Provider"]) class ProviderViewSet(BaseRLSViewSet): pass
API Security Patterns
Full examples: See assets/security_patterns.py
| Pattern | Key Points |
|---|---|
| Input Validation | Use for sanitization, for cross-field |
| Prevent Mass Assignment | ALWAYS use explicit list, NEVER or |
| Object-Level Permissions | Implement for ownership checks |
| Rate Limiting | Configure , use per-view throttles for sensitive endpoints |
| Prevent Info Disclosure | Generic error messages, return 404 not 403 for unauthorized (prevents enumeration) |
| SQL Injection | ALWAYS use ORM parameterization, NEVER string interpolation in raw SQL |
Quick Reference
# Input validation in serializer def validate_uid(self, value): value = value.strip().lower() if not re.match(r'^[a-z0-9-]+$', value): raise serializers.ValidationError("Invalid format") return value # Explicit fields (prevent mass assignment) class Meta: fields = ["name", "email"] # GOOD: whitelist read_only_fields = ["id", "inserted_at"] # System fields # Object permission class IsOwnerOrReadOnly(BasePermission): def has_object_permission(self, request, view, obj): if request.method in SAFE_METHODS: return True return obj.owner == request.user # Throttling for sensitive endpoints class BurstRateThrottle(UserRateThrottle): rate = "10/minute" # Safe error messages (prevent enumeration) def get_object(self): try: return super().get_object() except Http404: raise NotFound("Resource not found") # Generic, no internal IDs
Commands
# Development cd api && poetry run python src/backend/manage.py runserver cd api && poetry run python src/backend/manage.py shell # Database cd api && poetry run python src/backend/manage.py makemigrations cd api && poetry run python src/backend/manage.py migrate # Testing cd api && poetry run pytest -x --tb=short cd api && poetry run make lint
Resources
Local References
- File Locations: See references/file-locations.md
- JSON:API Conventions: See references/json-api-conventions.md
- Security Patterns: See assets/security_patterns.py
Context7 MCP (Recommended)
Prerequisite: Install Context7 MCP server for up-to-date documentation lookup.
When implementing or debugging, query these libraries via
mcp_context7_query-docs:
| Library | Context7 ID | Use For |
|---|---|---|
| Django | | Models, ORM, migrations |
| DRF | | ViewSets, serializers, permissions |
| drf-spectacular | | OpenAPI schema, |
Example queries:
mcp_context7_query-docs(libraryId="/websites/django-rest-framework", query="ViewSet get_queryset best practices") mcp_context7_query-docs(libraryId="/tfranzel/drf-spectacular", query="extend_schema examples for custom actions") mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_2", query="model constraints and indexes")
Note: Use
first if you need to find the correct library ID.mcp_context7_resolve-library-id
External Docs
- DRF Docs: https://www.django-rest-framework.org/
- DRF JSON:API: https://django-rest-framework-json-api.readthedocs.io/
- drf-spectacular: https://drf-spectacular.readthedocs.io/
- django-filter: https://django-filter.readthedocs.io/