DRF advanced cheatsheet.
JWT auth (simplejwt)
uv add djangorestframework-simplejwt
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
}
# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path("api/token/", TokenObtainPairView.as_view()),
path("api/token/refresh/", TokenRefreshView.as_view()),
]
curl -X POST /api/token/ -d '{"username":"u","password":"p"}' -H "Content-Type: application/json"
# {"access": "...", "refresh": "..."}
curl /api/posts/ -H "Authorization: Bearer <access>"
Throttling
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/hour",
"user": "1000/hour",
"burst": "60/min",
"sustained": "1000/day",
},
}
Per-view:
from rest_framework.throttling import ScopedRateThrottle
class LoginView(APIView):
throttle_classes = [ScopedRateThrottle]
throttle_scope = "login"
"DEFAULT_THROTTLE_RATES": { "login": "5/min" }
drf-spectacular (OpenAPI)
uv add drf-spectacular
INSTALLED_APPS = [..., "drf_spectacular"]
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "My API",
"DESCRIPTION": "API for my service",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
}
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/docs/", SpectacularSwaggerView.as_view()),
]
Documenting via extend_schema
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
@extend_schema(
parameters=[
OpenApiParameter(name="q", description="search", type=OpenApiTypes.STR),
],
responses={200: PostSerializer(many=True)},
)
def list(self, request):
...
@extend_schema(
description="Bulk delete posts",
request={"application/json": {"type": "array", "items": {"type": "integer"}}},
)
@action(detail=False, methods=["post"])
def bulk_delete(self, request):
...
Optimizing list endpoints
class PostViewSet(viewsets.ReadOnlyModelViewSet):
queryset = (
Post.objects
.select_related("author")
.prefetch_related("tags", "comments")
.annotate(comment_count=Count("comments"))
)
serializer_class = PostSerializer
Use a lighter serializer for list:
class PostListSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ["id", "title", "author_name", "comment_count"]
class PostDetailSerializer(PostListSerializer):
class Meta:
model = Post
fields = "__all__"
class PostViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action == "list":
return PostListSerializer
return PostDetailSerializer
SerializerMethodField
class PostSerializer(serializers.ModelSerializer):
is_liked = serializers.SerializerMethodField()
def get_is_liked(self, obj):
user = self.context["request"].user
if not user.is_authenticated: return False
return obj.likes.filter(user=user).exists()
class Meta:
model = Post
fields = [..., "is_liked"]
Watch for N+1 — annotate when possible.
Bulk operations
class BulkCreateView(generics.CreateAPIView):
serializer_class = PostSerializer
def create(self, request):
many = isinstance(request.data, list)
s = self.get_serializer(data=request.data, many=many)
s.is_valid(raise_exception=True)
s.save()
return Response(s.data, status=201)
File upload
from rest_framework.parsers import MultiPartParser
class UploadView(APIView):
parser_classes = [MultiPartParser]
def post(self, request):
file = request.FILES["file"]
...
Versioning
REST_FRAMEWORK = {
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_VERSION": "v1",
"ALLOWED_VERSIONS": ["v1", "v2"],
}
urlpatterns = [
path("api/<str:version>/posts/", PostList.as_view()),
]
def get_serializer_class(self):
if self.request.version == "v2":
return PostV2Serializer
return PostV1Serializer
Exception handler
def custom_exception_handler(exc, context):
from rest_framework.views import exception_handler
response = exception_handler(exc, context)
if response is not None:
response.data = {
"error": str(exc),
"code": response.status_code,
}
return response
REST_FRAMEWORK = {
"EXCEPTION_HANDLER": "myapp.errors.custom_exception_handler",
}
CORS
uv add django-cors-headers
INSTALLED_APPS = [..., "corsheaders"]
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware", ...]
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_CREDENTIALS = True
Caching responses
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
@method_decorator(cache_page(60 * 5), name="list")
class PostViewSet(viewsets.ReadOnlyModelViewSet):
...
Or per-action with Vary header.
Common mistakes
- N+1 in serializer’s nested fields → slow API.
- Same serializer for list and detail → bloated list response.
- Throttling not configured per-IP for anon.
- JWT secret in source — leak risk.
- Versioning by URL but mixing v1/v2 serializers without tests.
Read this next
If you want my DRF + JWT + OpenAPI starter, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .