django-rest-framework — Cursor rule: DRF API standards
A Cursor rule that gives the agent one consistent way to build Django REST Framework APIs: project layout, serializers, ViewSets.
.cursor/rules/django-rest-framework.mdcWhat django-rest-framework does
The file .cursor/rules/django-rest-framework.mdc plugs into Cursor and applies to every .py file in the project. Inside is a set of requirements for DRF APIs: where the logic lives, how global settings are wired, which classes to reach for on CRUD. The agent reads those requirements and proposes code in the same shape instead of inventing a new one each time.
On structure, the rule wants a dedicated api Django app, an api_settings.py holding DRF settings, and a DefaultRouter in urls.py. Serializers should be ModelSerializer for CRUD and a custom Serializer for non-model data, with select_related and prefetch_related to keep N+1 queries out. Endpoints go through ModelViewSet and ReadOnlyModelViewSet. The settings examples pull in rest_framework_simplejwt, django_filters and drf_spectacular — if your stack doesn't have them, expect to adapt part of what the agent suggests.
The rule is opinionated: it dictates a specific file layout and specific libraries. In a project where the API is already organised differently, the agent will start proposing moves and renames. Worth checking before you accept those edits.
What it helps with
- give the agent one code style for a DRF API
- move endpoints into ViewSets and DefaultRouter instead of hand-written APIView
- kill N+1 queries with select_related and prefetch_related
- keep global DRF settings in a separate file
- wire up JWT auth and filtering in settings
Example prompts for the agent
- Create a ViewSet for the Product model following the project rules
- Lay out serializers for Category and Review the way this repo does it
- Set up
api_settings.pyand import it intosettings.py
How to install django-rest-framework
Manually: put the files in place
.cursor/rules/django-rest-framework.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/django-rest-framework.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/django-rest-framework.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/django-rest-framework.mdManually: put the files in place
.clinerules/django-rest-framework.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about django-rest-framework
How do I install django-rest-framework in Cursor?
Create .cursor/rules/django-rest-framework.mdc in your project root and paste the rule contents into it. Cursor picks it up automatically.
What does this rule do?
It sets standards for DRF API development: the api app layout, serializer and ViewSet usage, authentication and pagination settings.
Does it need extra libraries?
The rule's examples use rest_framework_simplejwt, django_filters and drf_spectacular. Without them some of the suggested code won't run.
Source text
django-rest-framework.mdc · CC0-1.0
---
description: Enforces modern, secure, and performant `django-rest-framework` best practices for API development, focusing on maintainability and scalability.
globs: **/*.py
---
# django-rest-framework Best Practices
This guide outlines our definitive standards for building APIs with Django REST Framework (DRF). Adhere to these rules to ensure consistency, security, and performance across all projects.
## 1. Code Organization and Structure
Always structure your API within a dedicated Django app, typically named `api`. Centralize DRF-specific settings and use `DefaultRouter` for URL management.
- **Dedicated `api/` app**: Isolate API logic.
- **`api_settings.py`**: Store all DRF global settings here, then import into `settings.py`.
- **`urls.py` with `DefaultRouter`**: Automate URL patterns for ViewSets.
```python
# project_root/api/apps.py
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'
# project_root/api/settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 25,
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', # For OpenAPI
}
# project_root/settings.py
from .api.settings import REST_FRAMEWORK as API_REST_FRAMEWORK
REST_FRAMEWORK = API_REST_FRAMEWORK
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework_simplejwt',
'django_filters',
'drf_spectacular', # For OpenAPI
'api',
]
# project_root/api/urls.py
from rest_framework.routers import DefaultRouter
from .views import UserViewSet, ProductViewSet
from django.urls import path, include
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
router.register(r'products', ProductViewSet, basename='product')
urlpatterns = [
path('', include(router.urls)),
]
# project_root/project_name/urls.py
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('api.urls')), # Versioned API namespace
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
]
```
## 2. Serializers: Efficiency and Validation
Prioritize `ModelSerializer` for CRUD operations. Use custom `Serializer` classes only for complex, non-model-backed data or advanced validation. Always use `select_related`/`prefetch_related` to prevent N+1 queries.
```python
from rest_framework import serializers
from django.db.models import QuerySet
from typing import Any
from .models import Product, Category, Review, User
# ❌ BAD: N+1 queries for related objects, exposing all fields
class BadProductSerializer(serializers.ModelSerializer):
category = serializers.CharField(source='category.name')
reviews = serializers.SerializerMethodField()
class Meta:
model = Product
fields = ['id', 'name', 'price', 'category', 'reviews']
def get_reviews(self, obj: Product) -> list[dict[str, Any]]:
return [{'text': r.text, 'rating': r.rating} for r in obj.reviews.all()]
# ✅ GOOD: Optimized queries, controlled field exposure, explicit types
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name']
class ReviewSerializer(serializers.ModelSerializer):
user = serializers.SlugRelatedField(slug_field='username', read_only=True)
class Meta:
model = Review
fields = ['id', 'user', 'text', 'rating', 'created_at']
read_only_fields = ['user', 'created_at']
class ProductSerializer(serializers.ModelSerializer):
category: CategorySerializer = CategorySerializer(read_only=True) # Type hint for nested serializer
reviews: ReviewSerializer = ReviewSerializer(many=True, read_only=True) # Type hint for nested list
class Meta:
model = Product
fields = ['id', 'name', 'price', 'category', 'reviews', 'stock_count']
read_only_fields = ['stock_count'] # Fields that should not be updated via API
def create(self, validated_data: dict[str, Any]) -> Product:
# Custom logic if needed, e.g., setting owner
validated_data['owner'] = self.context['request'].user
return super().create(validated_data)
# ✅ GOOD: Custom serializer for non-model data or specific input validation
class UserRegistrationSerializer(serializers.Serializer):
username: str = serializers.CharField(max_length=150)
email: str = serializers.EmailField()
password: str = serializers.CharField(write_only=True)
password_confirm: str = serializers.CharField(write_only=True)
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
if data['password'] != data['password_confirm']:
raise serializers.ValidationError({"password_confirm": "Passwords do not match."})
return data
def create(self, validated_data: dict[str, Any]) -> User:
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data['email'],
password=validated_data['password']
)
return user
```
## 3. ViewSets & Routers: Streamlining Endpoints
Always use `ModelViewSet` for standard CRUD operations. For read-only resources, use `ReadOnlyModelViewSet`. Leverage `DefaultRouter` to automatically generate URLs.
```python
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from django.db.models import QuerySet
from typing import Any
from .models import Product, Category, User
from .serializers import ProductSerializer, CategorySerializer, UserRegistrationSerializer, ReviewSerializer
# ❌ BAD: Manual URL routing, repetitive logic for each HTTP method
# class ProductList(APIView):
# def get(self, request): ...
# def post(self, request): ...
# ✅ GOOD: ModelViewSet for full CRUD, optimized queryset, explicit permissions
class ProductViewSet(viewsets.ModelViewSet):
queryset: QuerySet[Product] = Product.objects.select_related('category').prefetch_related('reviews__user').all()
serializer_class: type[ProductSerializer] = ProductSerializer
permission_classes: list[type[IsAuthenticated | IsAdminUser]] = [IsAuthenticated, IsAdminUser] # Example: Admin can manage products
def get_queryset(self) -> QuerySet[Product]:
# Optimize queryset for specific actions if needed
if self.action == 'list':
return self.queryset.filter(is_active=True)
return self.queryset
def perform_create(self, serializer: ProductSerializer) -> None:
serializer.save(owner=self.request.user) # Automatically assign owner
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def add_review(self, request: Any, pk: int | None = None) -> Response:
product = self.get_object()
serializer = ReviewSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(product=product, user=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
# ✅ GOOD: ReadOnlyModelViewSet for read-only resources
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
queryset: QuerySet[Category] = Category.objects.all()
serializer_class: type[CategorySerializer] = CategorySerializer
permission_classes: list[type[IsAuthenticated]] = [IsAuthenticated]
# ✅ GOOD: Custom ViewSet for non-model operations (e.g., user registration)
class UserViewSet(viewsets.ViewSet):
permission_classes: list[type[IsAuthenticated]] = [IsAuthenticated] # Adjust as needed for registration
@action(detail=False, methods=['post'], permission_classes=[]) # No auth for registration
def register(self, request: Any) -> Response:
serializer = UserRegistrationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
return Response({"message": "User registered successfully."}, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['get'])
def me(self, request: Any) -> Response:
serializer = ProductSerializer(request.user) # Example: reuse ProductSerializer for user details
return Response(serializer.data)
```
## 4. Authentication & Permissions: Secure Access
Always use token-based authentication (JWT or DRF's `TokenAuthentication`) for stateless APIs. Implement granular permissions using DRF's built-in classes or custom ones. Never use `AllowAny` unless the endpoint is truly public.
```python
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.request import Request
from typing import Any
# ✅ GOOD: Custom object-level permission
class IsOwnerOrReadOnly(BasePermission):
"""
Object-level permission to allow only owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
if request.method in SAFE_METHODS:
return True
return obj.owner == request.user
# Apply to ViewSet:
# class ProductViewSet(viewsets.ModelViewSet):
# permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
```
## 5. Pagination, Filtering & Versioning
Enable `PageNumberPagination` (default) or `CursorPagination` for large datasets. Use `django-filter` for query parameter filtering. Implement URL versioning (`/api/v1/`) for clear API evolution.
```python
# project_root/api/filters.py
import django_filters
from .models import Product
class ProductFilter(django_filters.FilterSet):
min_price = django_filters.NumberFilter(field_name="price", lookup_expr='gte')
max_price = django_filters.NumberFilter(field_name="price", lookup_expr='lte')
category_name = django_filters.CharFilter(field_name="category__name", lookup_expr='icontains')
class Meta:
model = Product
fields = ['category', 'is_active']
# Apply to ViewSet:
# class ProductViewSet(viewsets.ModelViewSet):
# filterset_class = ProductFilter
```
## 6. Performance Considerations
Prevent N+1 queries by using `select_related` and `prefetch_related` in your `get_queryset` method.
```python
# ✅ GOOD: Optimized queryset in ViewSet
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_queryset(self) -> QuerySet[Product]:
# Always optimize for common list/retrieve actions
return self.queryset.select_related('category').prefetch_related('reviews__user')
```
## 7. Error Handling
Leverage DRF's built-in exception handling. For custom errors, define a custom exception handler that returns consistent, machine-readable responses.
```python
# project_root/api/utils.py
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status
from typing import Any
def custom_exception_handler(exc: Exception, context: dict[str, Any]) -> Response | None:
# Call DRF's default exception handler first
response = exception_handler(exc, context)
if response is not None:
# Example: Customize validation error messages
if response.status_code == status.HTTP_400_BAD_REQUEST and 'detail' not in response.data:
response.data = {'errors': response.data}
return response
# Handle unhandled exceptions gracefully
return Response(
{'detail': 'An unexpected error occurred.'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# project_root/api/settings.py
REST_FRAMEWORK = {
# ...
'EXCEPTION_HANDLER': 'api.utils.custom_exception_handler',
}
```
## 8. Type Hints
Mandate type hints for all DRF components: serializers, views, viewsets, and custom permissions. This improves code readability, maintainability, and enables static analysis.
```python
# All examples above demonstrate mandatory type hints.
# Example:
# class ProductSerializer(serializers.ModelSerializer):
# category: CategorySerializer = CategorySerializer(read_only=True)
# def create(self, validated_data: dict[str, Any]) -> Product: ...
```
## 9. Rate Limiting
Implement DRF's throttling classes to protect your API from abuse.
```python
# project_root/api/settings.py
REST_FRAMEWORK = {
# ...
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day',
'burst': '60/min', # Custom throttle rate
}
}
# Apply to ViewSet for custom throttling:
# class ProductViewSet(viewsets.ModelViewSet):
# throttle_classes = [UserRateThrottle, AnonRateThrottle] # Or custom throttle
# throttle_scope = 'burst' # Use the 'burst' rate defined in settings
```
## 10. Async Support
For high-throughput endpoints, isolate async code within `async def` views or specific `ViewSet` actions. Run your project with an ASGI server (Uvicorn/Daphne). DRF's native async support is evolving; consider `adrf` for full async ViewSets if truly necessary, but be aware it's still experimental.
```python
from rest_framework.views import APIView
from rest_framework.response import Response
import asyncio
import httpx # For async HTTP requests
from typing import Any
# ✅ GOOD: Isolate async logic to specific views/actions
class AsyncDataView(APIView):
async def get(self, request: Any, *args: Any, **kwargs: Any) -> Response:
# Simulate an async operation, e.g., calling an external async service
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/external-data")
response.raise_for_status()
data = response.json()
return Response({"message": "Data fetched asynchronously", "data": data})
# project_root/api/urls.py
# from .views import AsyncDataView
# urlpatterns = [
# path('async-data/', AsyncDataView.as_view()),
# # ...
# ]
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc