Provides standard patterns and architectural rules for Django REST Framework APIs.

Install

mkdir -p .claude/skills/django-drf && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/3417" && unzip -o skill.zip -d .claude/skills/django-drf && rm skill.zip

Installs to .claude/skills/django-drf

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Django REST Framework patterns. Trigger: When implementing generic DRF APIs (ViewSets, serializers, routers, permissions, filtersets). For Prowler API specifics (RLS/RBAC/Providers), also use prowler-api.
204 chars · catalog description✓ has a “when” trigger
Intermediate

Key capabilities

  • Implement DRF ViewSets and serializers
  • Enforce N+1 query optimization using select_related and prefetch_related
  • Generate OpenAPI documentation with drf-spectacular
  • Apply RLS-protected model patterns
  • Configure JSON:API compliant endpoints
  • Manage sensitive data masking in serializers

How it works

The skill provides a structured checklist and pattern library for DRF components, ensuring adherence to N+1 prevention and schema generation standards. It mandates specific base class hierarchies for read, write, and include operations.

Inputs & outputs

You give it
Django model definitions and API requirements
You get back
DRF ViewSets, serializers, and OpenAPI-compliant endpoints

When to use django-drf

  • Creating ViewSets and serializers
  • Optimizing database queries to avoid N+1
  • Implementing DRF permissions and filtering
  • Generating OpenAPI documentation

About this skill

Critical Patterns

  • ALWAYS separate serializers by operation: Read / Create / Update / Include
  • ALWAYS use filterset_class for complex filtering (not filterset_fields)
  • ALWAYS validate unknown fields in write serializers (inherit BaseWriteSerializer)
  • ALWAYS use select_related/prefetch_related in get_queryset() to avoid N+1
  • ALWAYS handle swagger_fake_view in get_queryset() for schema generation
  • ALWAYS use @extend_schema_field for OpenAPI docs on SerializerMethodField
  • 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: swagger_fake_view is specific to drf-spectacular for OpenAPI schema generation.


Implementation Checklist

When implementing a new endpoint, review these patterns in order:

#PatternReferenceKey Points
1Modelsapi/models.pyUUID PK, inserted_at/updated_at, JSONAPIMeta.resource_name
2ViewSetsapi/base_views.py, api/v1/views.pyInherit BaseRLSViewSet, get_queryset() with N+1 prevention
3Serializersapi/v1/serializers.pySeparate Read/Create/Update/Include, inherit BaseWriteSerializer
4Filtersapi/filters.pyUse filterset_class, inherit base filter classes
5Permissionsapi/base_views.pyrequired_permissions, set_required_permissions()
6Paginationapi/pagination.pyCustom pagination class if needed
7URL Routingapi/v1/urls.pytrailing_slash=False, kebab-case paths
8OpenAPI Schemaapi/v1/views.py@extend_schema_view with drf-spectacular
9Testsapi/tests/test_views.pyJSON: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):
      

---

*Content truncated.*

When not to use it

  • When implementing Prowler-specific API logic requiring prowler-api
  • When using auto-increment primary keys instead of UUIDs

Prerequisites

Context7 MCP server

Limitations

  • Requires manual adherence to the implementation checklist
  • Strictly forbids the use of trailing slashes in URLs

How it compares

Unlike generic DRF tutorials, this skill enforces strict Prowler-specific architectural patterns like UUIDv4 usage and mandatory swagger_fake_view handling.

Compared to similar skills

django-drf side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
django-drf (this skill)62moReviewIntermediate
django-patterns34moNo flagsIntermediate
api-integration02moNo flagsIntermediate
htmx-patterns04moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry