
Django Drf
- 462 installs
- 14.5k repo stars
- Updated August 4, 2026
- prowler-cloud/prowler
django-drf is a Django REST Framework patterns skill at version 1.2.0 that generates consistent ViewSets, serializers, routers, permissions, and filtersets for developers building generic JSON APIs.
About
django-drf is an Apache-2.0 skill from prowler-cloud/prowler at version 1.2.0 that encodes Django REST Framework patterns for generic API development. It auto-invokes when creating ViewSets, serializers, or filters in api/, implementing JSON:API endpoints, or adding DRF pagination and permissions. Critical rules include always separating serializers by operation—Read, Create, Update, and Include—and using filtersets consistently. The skill scopes to root and api directories and allows Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, and Task tools. For Prowler-specific API concerns such as RLS, RBAC, and Providers, developers should also invoke the prowler-api skill alongside django-drf.
- 9 critical DRF patterns including separate read/write serializers and filterset_class usage
- Implementation checklist that enforces 9 patterns in order for every new endpoint
- Prevents N+1 queries by always using select_related and prefetch_related
- Enforces UUIDv4/v7 instead of auto-increment PKs and removes business logic from serializers
- Handles swagger_fake_view for drf-spectacular OpenAPI generation automatically
Django Drf by the numbers
- 462 all-time installs (skills.sh)
- Ranked #919 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/prowler-cloud/prowler --skill django-drfAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 462 |
|---|---|
| repo stars | ★ 14.5k |
| Last updated | August 4, 2026 |
| Repository | prowler-cloud/prowler ↗ |
How do you implement DRF ViewSets and serializers correctly?
Generate correct, consistent Django REST Framework code patterns when building APIs.
Who is it for?
Backend developers implementing generic Django REST Framework APIs who need consistent ViewSet, serializer, and filterset patterns from prowler-cloud.
Skip if: Prowler-specific RLS, RBAC, or Providers API logic without also invoking the dedicated prowler-api skill.
When should I use this skill?
A developer creates DRF ViewSets, serializers, filters, JSON:API endpoints, pagination, or permissions under api/.
What you get
DRF ViewSets, Read/Create/Update/Include serializers, routers, permissions, filtersets, and JSON:API-compliant endpoint scaffolding in api/.
- DRF ViewSet scaffolding
- Operation-split serializers
- Router and filterset configuration
By the numbers
- Version 1.2.0 in prowler-cloud/prowler metadata
- Requires four serializer operation types: Read, Create, Update, Include
Files
Critical Patterns
- ALWAYS separate serializers by operation: Read / Create / Update / Include
- ALWAYS use
filterset_classfor complex filtering (notfilterset_fields) - ALWAYS validate unknown fields in write serializers (inherit
BaseWriteSerializer) - ALWAYS use
select_related/prefetch_relatedinget_queryset()to avoid N+1 - ALWAYS handle
swagger_fake_viewinget_queryset()for schema generation - ALWAYS use
@extend_schema_fieldfor OpenAPI docs onSerializerMethodField - 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:
| # | Pattern | Reference | Key Points |
|---|---|---|---|
| 1 | Models | api/models.py | UUID PK, inserted_at/updated_at, JSONAPIMeta.resource_name |
| 2 | ViewSets | api/base_views.py, api/v1/views.py | Inherit BaseRLSViewSet, get_queryset() with N+1 prevention |
| 3 | Serializers | api/v1/serializers.py | Separate Read/Create/Update/Include, inherit BaseWriteSerializer |
| 4 | Filters | api/filters.py | Use filterset_class, inherit base filter classes |
| 5 | Permissions | api/base_views.py | required_permissions, set_required_permissions() |
| 6 | Pagination | api/pagination.py | Custom pagination class if needed |
| 7 | URL Routing | api/v1/urls.py | trailing_slash=False, kebab-case paths |
| 8 | OpenAPI Schema | api/v1/views.py | @extend_schema_view with drf-spectacular |
| 9 | Tests | api/tests/test_views.py | 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>IncludeSerializerWhich 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 → BaseSerializerV1Which Filter Base?
Direct FK to Provider → BaseProviderFilter
FK via Scan → BaseScanProviderFilter
No provider relation → FilterSetWhich 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 fieldsSerializerMethodField 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 ProviderSerializerDynamic 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):
passCustom 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):
passArrayField 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 FindingFilterEnum 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) | <Model>Serializer | ProviderSerializer |
| Serializer (create) | <Model>CreateSerializer | ProviderCreateSerializer |
| Serializer (update) | <Model>UpdateSerializer | ProviderUpdateSerializer |
| Serializer (include) | <Model>IncludeSerializer | ProviderIncludeSerializer |
| Filter | <Model>Filter | ProviderFilter |
| ViewSet | <Model>ViewSet | ProviderViewSet |
---
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 validate_<field>() for sanitization, validate() for cross-field |
| Prevent Mass Assignment | ALWAYS use explicit fields list, NEVER __all__ or exclude |
| Object-Level Permissions | Implement has_object_permission() for ownership checks |
| Rate Limiting | Configure DEFAULT_THROTTLE_RATES, 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 && uv run python src/backend/manage.py runserver
cd api && uv run python src/backend/manage.py shell
# Database
cd api && uv run python src/backend/manage.py makemigrations
cd api && uv run python src/backend/manage.py migrate
# Testing
cd api && uv run pytest -x --tb=short
cd api && uv 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 | /websites/djangoproject_en_5_2 | Models, ORM, migrations |
| DRF | /websites/django-rest-framework | ViewSets, serializers, permissions |
| drf-spectacular | /tfranzel/drf-spectacular | OpenAPI schema, @extend_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 mcp_context7_resolve-library-id first if you need to find the correct 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/
# Example: DRF API Security Patterns
# Reference for django-drf skill
import re
from rest_framework import serializers, status, viewsets
from rest_framework.exceptions import NotFound
from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAuthenticated
from rest_framework.throttling import UserRateThrottle
# =============================================================================
# INPUT VALIDATION
# =============================================================================
class ProviderCreateSerializer(serializers.Serializer):
"""Example: Input validation in serializers."""
uid = serializers.CharField(max_length=255)
provider = serializers.CharField()
def validate_uid(self, value):
"""Field-level validation with sanitization."""
# Sanitize: strip whitespace, normalize
value = value.strip().lower()
# Validate format
if not re.match(r"^[a-z0-9-]+$", value):
raise serializers.ValidationError(
"UID must be alphanumeric with hyphens only"
)
return value
def validate(self, attrs):
"""Cross-field validation."""
if attrs.get("provider") == "aws" and len(attrs.get("uid", "")) != 12:
raise serializers.ValidationError(
{"uid": "AWS account ID must be 12 digits"}
)
return attrs
# =============================================================================
# PREVENT MASS ASSIGNMENT
# =============================================================================
class UserUpdateSerializer(serializers.ModelSerializer):
"""Example: Explicit field whitelist prevents mass assignment."""
class Meta:
# GOOD: Explicit whitelist
fields = ["name", "email"]
# BAD: fields = "__all__" # Exposes is_staff, is_superuser
# BAD: exclude = ["password"] # New fields auto-exposed
class ProviderSerializer(serializers.ModelSerializer):
"""Example: Read-only fields for computed/system values."""
class Meta:
fields = ["id", "uid", "alias", "connected", "inserted_at"]
# Cannot be set via API - only read
read_only_fields = ["id", "connected", "inserted_at"]
# =============================================================================
# OBJECT-LEVEL PERMISSIONS
# =============================================================================
class IsOwnerOrReadOnly(BasePermission):
"""Example: Object-level permission check."""
def has_object_permission(self, request, view, obj):
# Read permissions for any authenticated request
if request.method in SAFE_METHODS:
return True
# Write permissions only for owner
return obj.owner == request.user
class DocumentViewSet(viewsets.ModelViewSet):
"""Example: ViewSet with object-level permissions."""
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
# =============================================================================
# RATE LIMITING (THROTTLING)
# =============================================================================
# In settings.py:
# REST_FRAMEWORK = {
# "DEFAULT_THROTTLE_CLASSES": [
# "rest_framework.throttling.AnonRateThrottle",
# "rest_framework.throttling.UserRateThrottle",
# ],
# "DEFAULT_THROTTLE_RATES": {
# "anon": "100/hour",
# "user": "1000/hour",
# },
# }
class BurstRateThrottle(UserRateThrottle):
"""Example: Custom throttle for sensitive endpoints."""
rate = "10/minute"
class PasswordResetViewSet(viewsets.ViewSet):
"""Example: Per-view throttling for sensitive endpoints."""
throttle_classes = [BurstRateThrottle]
# =============================================================================
# PREVENT INFORMATION DISCLOSURE
# =============================================================================
class SecureViewSet(viewsets.ModelViewSet):
"""Example: Prevent information disclosure patterns."""
def get_object(self):
try:
return super().get_object()
except Exception:
# GOOD: Generic message - doesn't leak internal IDs or tenant info
raise NotFound("Resource not found")
# BAD: raise NotFound(f"Provider {pk} not found in tenant {tenant_id}")
def get_queryset(self):
# Use 404 not 403 for unauthorized access (prevents enumeration)
# Filter by tenant - unauthorized users get 404, not 403
return self.queryset.filter(tenant_id=self.request.tenant_id)
# =============================================================================
# SQL INJECTION PREVENTION
# =============================================================================
def safe_query_examples(user_input):
"""Example: SQL injection prevention patterns."""
from django.db import connection
# GOOD: Parameterized via ORM
# Provider.objects.filter(uid=user_input)
# Provider.objects.extra(where=["uid = %s"], params=[user_input])
# GOOD: If raw SQL unavoidable, use parameterized queries
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM providers WHERE uid = %s", [user_input])
# BAD: String interpolation = SQL injection vulnerability
# Provider.objects.raw(f"SELECT * FROM providers WHERE uid = '{user_input}'")
# cursor.execute(f"SELECT * FROM providers WHERE uid = '{user_input}'")
Django-DRF File Locations
Core API Files
| Pattern | File Path | Key Classes |
|---|---|---|
| Models | api/src/backend/api/models.py | Provider, Scan, Finding, Resource, StateChoices, StatusChoices |
| ViewSets | api/src/backend/api/v1/views.py | BaseViewSet, BaseRLSViewSet, BaseTenantViewset, BaseUserViewset |
| Serializers | api/src/backend/api/v1/serializers.py | BaseModelSerializerV1, BaseWriteSerializer, RLSSerializer |
| Filters | api/src/backend/api/filters.py | BaseProviderFilter, BaseScanProviderFilter, CommonFindingFilters |
| URL Routing | api/src/backend/api/v1/urls.py | Router setup, nested routes |
| Pagination | api/src/backend/api/pagination.py | LimitedJsonApiPageNumberPagination |
| Permissions | api/src/backend/api/decorators.py | HasPermissions, @check_permissions |
| RBAC | api/src/backend/api/rbac/permissions.py | Permissions enum, get_role(), get_providers() |
| Settings | api/src/backend/config/settings.py | REST_FRAMEWORK config |
ViewSet Hierarchy
BaseViewSet (minimal - no RLS/auth)
│
├── BaseRLSViewSet (+ tenant filtering, RLS-protected models)
│ └── Most ViewSets inherit this
│
├── BaseTenantViewset (+ Tenant-specific logic)
│ └── TenantViewSet
│
└── BaseUserViewset (+ User-specific logic)
└── UserViewSetSerializer Hierarchy
BaseModelSerializerV1 (JSON:API defaults, read_only_fields)
│
├── RLSSerializer (auto-injects tenant_id from request)
│ └── Most model serializers inherit this
│
└── BaseWriteSerializer (rejects unknown fields)
└── Create/Update serializers
+ Mixins:
- IncludedResourcesValidationMixin (validates ?include= param)
- JSONAPIRelatedLinksSerializerMixin (adds related links)Filter Hierarchy
FilterSet (django-filter)
│
├── CommonFindingFilters (mixin for date ranges, delta, status)
│
├── BaseProviderFilter (provider_type, provider_uid, provider_alias)
│ │
│ └── BaseScanProviderFilter (+ scan_id, scan filters)
│
└── Resource-specific filters (ProviderFilter, ScanFilter, etc.)
Custom Filter Types:
- UUIDInFilter: Comma-separated UUIDs
- CharInFilter: Comma-separated strings
- DateFilter: ISO date parsing
- DateTimeFilter: ISO datetime parsingTesting Files
| Pattern | File Path | Key Classes |
|---|---|---|
| ViewSet Tests | api/src/backend/api/tests/test_views.py | Test patterns, fixtures |
| RBAC Tests | api/src/backend/api/tests/test_rbac.py | Permission tests |
| Serializer Tests | api/src/backend/api/tests/test_serializers.py | Validation tests |
| Conftest | api/src/backend/conftest.py | Shared fixtures |
Key Patterns
Filter Usage
# In filters.py
class ProviderFilter(BaseProviderFilter):
class Meta:
model = Provider
fields = {
"provider": ["exact", "in"],
"connected": ["exact"],
}
# Custom filter method
def filter_severity(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(severity__in=value)Serializer Usage
# Read serializer
class ProviderSerializer(RLSSerializer):
class Meta:
model = Provider
fields = ["id", "provider", "uid", "alias", "connected"]
# Write serializer
class ProviderCreateSerializer(BaseWriteSerializer, RLSSerializer):
class Meta:
model = Provider
fields = ["provider", "uid", "alias"]ViewSet Action Pattern
@action(detail=True, methods=["post"], url_path="scan")
def trigger_scan(self, request, pk=None):
provider = self.get_object()
task = perform_scan_task.delay(...)
return Response(status=status.HTTP_202_ACCEPTED)REST_FRAMEWORK Settings
Located in api/src/backend/config/settings.py:
REST_FRAMEWORK = {
"PAGE_SIZE": 10,
"DEFAULT_PAGINATION_CLASS": "api.pagination.LimitedJsonApiPageNumberPagination",
"DEFAULT_PARSER_CLASSES": [
"rest_framework_json_api.parsers.JSONParser",
"rest_framework.parsers.JSONParser",
],
"DEFAULT_FILTER_BACKENDS": [
"rest_framework_json_api.filters.QueryParameterValidationFilter",
"rest_framework_json_api.filters.OrderingFilter",
"rest_framework_json_api.django_filters.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
],
"EXCEPTION_HANDLER": "rest_framework_json_api.exceptions.exception_handler",
# ... more settings
}JSON:API Resource Names
Find all JSONAPIMeta declarations:
rg "resource_name" api/src/backend/api/models.pyConvention: kebab-case, plural (e.g., provider-groups, mute-rules)
JSON:API Conventions
Content Type
Content-Type: application/vnd.api+json
Accept: application/vnd.api+jsonQuery Parameters
| Feature | Format | Example |
|---|---|---|
| Pagination | page[number], page[size] | ?page[number]=2&page[size]=20 |
| Filtering | filter[field], filter[field__lookup] | ?filter[status]=FAIL&filter[inserted_at__gte]=2024-01-01 |
| Sorting | sort (prefix - for desc) | ?sort=-inserted_at,name |
| Sparse fields | fields[type] | ?fields[providers]=id,alias,uid |
| Includes | include | ?include=provider,scan |
| Search | filter[search] | ?filter[search]=production |
Filter Naming
| Lookup | Django Filter | JSON:API Query |
|---|---|---|
| Exact | field | filter[field]=value |
| Contains | field__icontains | filter[field__icontains]=val |
| In list | field__in | filter[field__in]=a,b,c |
| Greater/equal | field__gte | filter[field__gte]=2024-01-01 |
| Less/equal | field__lte | filter[field__lte]=2024-12-31 |
| Related field | relation__field | filter[provider_id]=uuid |
Request Format
{
"data": {
"type": "providers",
"attributes": {
"provider": "aws",
"uid": "123456789012",
"alias": "Production"
}
}
}Response Format
{
"data": {
"type": "providers",
"id": "550e8400-e29b-41d4-a716-446655440000",
"attributes": {
"provider": "aws",
"uid": "123456789012",
"alias": "Production",
"inserted_at": "2024-01-15T10:30:00Z"
},
"relationships": {
"provider_groups": {
"data": [{"type": "provider-groups", "id": "..."}]
}
},
"links": {
"self": "/api/v1/providers/550e8400-e29b-41d4-a716-446655440000"
}
},
"meta": {
"version": "v1"
}
}Error Response Format
{
"errors": [
{
"detail": "Error message here",
"status": "400",
"source": {"pointer": "/data/attributes/field_name"},
"code": "error_code"
}
]
}Resource Naming Rules
- Use lowercase kebab-case (hyphens, not underscores)
- Use plural nouns for collections
- Resource name in
JSONAPIMetaMUST match URL path segment
| Model | resource_name | URL Path |
|---|---|---|
Provider | providers | /api/v1/providers |
ProviderGroup | provider-groups | /api/v1/provider-groups |
ProviderSecret | provider-secrets | /api/v1/providers/secrets |
ComplianceOverview | compliance-overviews | /api/v1/compliance-overviews |
AttackPathsScan | attack-paths-scans | /api/v1/attack-paths-scans |
TenantAPIKey | api-keys | /api/v1/api-keys |
MuteRule | mute-rules | /api/v1/mute-rules |
URL Endpoints
| Operation | Method | URL Pattern |
|---|---|---|
| List | GET | /{resources} |
| Create | POST | /{resources} |
| Retrieve | GET | /{resources}/{id} |
| Update | PATCH | /{resources}/{id} |
| Delete | DELETE | /{resources}/{id} |
| Relationship | * | /{resources}/{id}/relationships/{relation} |
| Nested list | GET | /{parent}/{parent_id}/{resources} |
Related skills
How it compares
Use django-drf for generic DRF scaffolding and add prowler-api when implementing Prowler-specific authorization and provider endpoints.
FAQ
What serializer structure does django-drf require?
django-drf requires always separating serializers by operation: Read, Create, Update, and Include. The skill at version 1.2.0 from prowler-cloud encodes this as a critical DRF pattern for generic APIs.
When should prowler-api be used with django-drf?
django-drf covers generic DRF ViewSets, serializers, routers, and filtersets. prowler-api should also run for Prowler-specific concerns including RLS, RBAC, and Providers API implementation details.