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 .