Django REST Framework: The Complete Guide for 2026

Published February 12, 2026 · 25 min read

Django REST Framework (DRF) is the industry-standard toolkit for building Web APIs with Django. It provides serializers for data conversion and validation, class-based views for handling HTTP methods, automatic URL routing, built-in authentication and permissions, and everything else you need to ship production APIs without reinventing the wheel.

This guide covers DRF from installation to production: serializers, ModelSerializer, views (APIView through ModelViewSet), routers, authentication (Token, JWT, Session), permissions, filtering, pagination, throttling, testing, and the best practices that separate hobby projects from production systems.

1. What Is Django REST Framework

DRF sits on top of Django and adds everything you need to build RESTful APIs. While Django handles models, migrations, the ORM, and admin, DRF provides the API layer: serialization (converting model instances to JSON and back), request parsing, response rendering, authentication, permissions, and browsable API documentation out of the box.

DRF is used by companies like Mozilla, Red Hat, Eventbrite, and thousands of startups. It has over 28,000 GitHub stars and a mature ecosystem of third-party packages for JWT authentication, API documentation (drf-spectacular), filtering (django-filter), and more.

You should use DRF when you need to build a REST API for a single-page application frontend, mobile app, or third-party integrations on top of an existing Django project. If you are starting a new API-only project and do not need Django's template system or admin, consider FastAPI as a lighter alternative.

2. Installation and Setup

Install DRF into your Django project:

pip install djangorestframework

Add it to INSTALLED_APPS in your Django settings:

# settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",       # add DRF
    "myapp",                # your app
]

# Global DRF configuration
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
    "DEFAULT_RENDERER_CLASSES": [
        "rest_framework.renderers.JSONRenderer",
        "rest_framework.renderers.BrowsableAPIRenderer",
    ],
}

Include the DRF login URLs for the browsable API (optional but helpful during development):

# urls.py
from django.urls import path, include

urlpatterns = [
    path("api/", include("myapp.urls")),
    path("api-auth/", include("rest_framework.urls")),  # browsable API login
]

We will use this model throughout the guide:

# myapp/models.py
from django.db import models
from django.conf import settings

class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    body = models.TextField()
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    status = models.CharField(
        max_length=20,
        choices=[("draft", "Draft"), ("published", "Published")],
        default="draft",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

3. Serializers

Serializers convert Django model instances and querysets into Python dictionaries (which DRF then renders as JSON) and handle the reverse: parsing incoming JSON, validating it, and producing validated data you can save. Think of them as Django forms for APIs.

A basic Serializer defines each field explicitly:

# myapp/serializers.py
from rest_framework import serializers

class ArticleSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    title = serializers.CharField(max_length=200)
    slug = serializers.SlugField()
    body = serializers.CharField()
    author = serializers.PrimaryKeyRelatedField(read_only=True)
    status = serializers.ChoiceField(choices=["draft", "published"])
    created_at = serializers.DateTimeField(read_only=True)

    def create(self, validated_data):
        return Article.objects.create(**validated_data)

    def update(self, instance, validated_data):
        for attr, value in validated_data.items():
            setattr(instance, attr, value)
        instance.save()
        return instance

Use the serializer to serialize (output) and deserialize (input) data:

# Serialization: model instance -> dict -> JSON
article = Article.objects.first()
serializer = ArticleSerializer(article)
serializer.data  # {"id": 1, "title": "...", "slug": "...", ...}

# Serializing multiple objects
articles = Article.objects.all()
serializer = ArticleSerializer(articles, many=True)
serializer.data  # [{"id": 1, ...}, {"id": 2, ...}]

# Deserialization: JSON -> validated dict -> model instance
data = {"title": "New Article", "slug": "new-article", "body": "Content", "status": "draft"}
serializer = ArticleSerializer(data=data)
serializer.is_valid(raise_exception=True)  # raises ValidationError on failure
article = serializer.save(author=request.user)

Field-Level and Object-Level Validation

class ArticleSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=200)
    slug = serializers.SlugField()
    status = serializers.ChoiceField(choices=["draft", "published"])

    # Field-level validation: validate_<field_name>
    def validate_title(self, value):
        if value.lower() == "untitled":
            raise serializers.ValidationError("Title cannot be 'Untitled'.")
        return value

    # Object-level validation: access all fields
    def validate(self, data):
        if data.get("status") == "published" and len(data.get("body", "")) < 100:
            raise serializers.ValidationError(
                "Published articles must have at least 100 characters."
            )
        return data

4. ModelSerializer

ModelSerializer auto-generates serializer fields from a Django model, including appropriate validators. This is what you will use 90% of the time:

from rest_framework import serializers
from myapp.models import Article

class ArticleSerializer(serializers.ModelSerializer):
    author_name = serializers.CharField(source="author.get_full_name", read_only=True)

    class Meta:
        model = Article
        fields = ["id", "title", "slug", "body", "author", "author_name",
                  "status", "created_at", "updated_at"]
        read_only_fields = ["id", "author", "created_at", "updated_at"]

Key Meta options: fields explicitly lists which fields to include (never use "__all__" in production since it exposes every field). read_only_fields marks fields that clients cannot set. extra_kwargs lets you customize individual field behavior:

class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ["id", "title", "slug", "body", "status", "author"]
        read_only_fields = ["id", "author"]
        extra_kwargs = {
            "body": {"min_length": 50, "help_text": "Article content (min 50 chars)"},
            "slug": {"validators": []},  # override default unique validator
        }

Nested Serializers

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "username", "email"]

class ArticleDetailSerializer(serializers.ModelSerializer):
    author = UserSerializer(read_only=True)  # nested representation

    class Meta:
        model = Article
        fields = ["id", "title", "slug", "body", "author", "status", "created_at"]

# Output:
# {
#   "id": 1,
#   "title": "My Article",
#   "author": {"id": 5, "username": "alice", "email": "alice@example.com"},
#   ...
# }

5. Views: APIView

APIView is the base class for DRF views. You define methods for each HTTP verb (get, post, put, delete). DRF handles content negotiation, request parsing, authentication, permissions, and exception handling:

# myapp/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from myapp.models import Article
from myapp.serializers import ArticleSerializer

class ArticleListView(APIView):
    def get(self, request):
        articles = Article.objects.filter(status="published")
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = ArticleSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save(author=request.user)
        return Response(serializer.data, status=status.HTTP_201_CREATED)

class ArticleDetailView(APIView):
    def get_object(self, slug):
        try:
            return Article.objects.get(slug=slug)
        except Article.DoesNotExist:
            raise NotFound("Article not found.")

    def get(self, request, slug):
        article = self.get_object(slug)
        serializer = ArticleSerializer(article)
        return Response(serializer.data)

    def put(self, request, slug):
        article = self.get_object(slug)
        serializer = ArticleSerializer(article, data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data)

    def delete(self, request, slug):
        article = self.get_object(slug)
        article.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Wire up the URLs:

# myapp/urls.py
from django.urls import path
from myapp.views import ArticleListView, ArticleDetailView

urlpatterns = [
    path("articles/", ArticleListView.as_view(), name="article-list"),
    path("articles/<slug:slug>/", ArticleDetailView.as_view(), name="article-detail"),
]

6. Generic Views and Mixins

DRF provides generic views that eliminate boilerplate for standard CRUD operations. They combine GenericAPIView (which adds queryset and serializer_class) with mixin classes:

from rest_framework import generics
from myapp.models import Article
from myapp.serializers import ArticleSerializer

# List + Create in one view
class ArticleListCreateView(generics.ListCreateAPIView):
    queryset = Article.objects.filter(status="published")
    serializer_class = ArticleSerializer

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

# Retrieve + Update + Destroy in one view
class ArticleDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    lookup_field = "slug"

Available generic views: ListAPIView, CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView, ListCreateAPIView, RetrieveUpdateAPIView, RetrieveDestroyAPIView, and RetrieveUpdateDestroyAPIView. Each combines the appropriate mixins automatically.

Override perform_create, perform_update, or perform_destroy to add custom logic (like setting the author or sending notifications) without rewriting the entire view.

7. ViewSets and Routers

ViewSets combine all related views (list, create, retrieve, update, destroy) into a single class. Routers then auto-generate the URL patterns:

# myapp/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from myapp.models import Article
from myapp.serializers import ArticleSerializer, ArticleDetailSerializer

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    lookup_field = "slug"

    def get_serializer_class(self):
        if self.action == "retrieve":
            return ArticleDetailSerializer
        return ArticleSerializer

    def get_queryset(self):
        qs = super().get_queryset()
        if self.action == "list":
            return qs.filter(status="published")
        return qs

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

    # Custom action: POST /api/articles/{slug}/publish/
    @action(detail=True, methods=["post"])
    def publish(self, request, slug=None):
        article = self.get_object()
        article.status = "published"
        article.save()
        return Response({"status": "published"})

Register the ViewSet with a router:

# myapp/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from myapp.views import ArticleViewSet

router = DefaultRouter()
router.register("articles", ArticleViewSet, basename="article")

urlpatterns = [
    path("", include(router.urls)),
]

# Generated URLs:
# GET    /articles/          -> list
# POST   /articles/          -> create
# GET    /articles/{slug}/   -> retrieve
# PUT    /articles/{slug}/   -> update
# PATCH  /articles/{slug}/   -> partial_update
# DELETE /articles/{slug}/   -> destroy
# POST   /articles/{slug}/publish/ -> publish (custom action)

Use ModelViewSet for full CRUD. Use ReadOnlyModelViewSet when clients should only read data. Use viewsets.ViewSet as a base for fully custom logic without model binding.

8. Authentication

DRF supports multiple authentication schemes. Configure them globally or per-view.

Token Authentication

# settings.py
INSTALLED_APPS = [..., "rest_framework.authtoken"]
# Run: python manage.py migrate

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
    ],
}

# Create tokens for users
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=user)
print(token.key)  # "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"

# Client sends: Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

Add a login endpoint that returns a token:

# urls.py
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    path("api/token/", obtain_auth_token),  # POST username + password, returns token
]

JWT Authentication (SimpleJWT)

JWT tokens are stateless and more suitable for distributed systems:

# pip install djangorestframework-simplejwt

# settings.py
from datetime import timedelta

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
    "SIGNING_KEY": SECRET_KEY,
    "AUTH_HEADER_TYPES": ("Bearer",),
}

# urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path("api/token/", TokenObtainPairView.as_view()),
    path("api/token/refresh/", TokenRefreshView.as_view()),
]

# Client usage:
# POST /api/token/ {"username": "alice", "password": "secret"}
#   -> {"access": "eyJ...", "refresh": "eyJ..."}
#
# GET /api/articles/ with header: Authorization: Bearer eyJ...
#
# POST /api/token/refresh/ {"refresh": "eyJ..."}
#   -> {"access": "new-eyJ..."}

Session Authentication

Session authentication uses Django's built-in session framework. It is enabled by default and works well for browser-based clients where your API and frontend share the same domain:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
}
# Session auth includes CSRF protection for non-safe methods (POST, PUT, DELETE)

9. Permissions

Permissions control who can access your API endpoints. DRF checks permissions after authentication:

# Built-in permission classes
from rest_framework.permissions import (
    AllowAny,              # no restrictions
    IsAuthenticated,       # logged-in users only
    IsAdminUser,           # staff users only (user.is_staff)
    IsAuthenticatedOrReadOnly,  # anyone can GET, auth required for POST/PUT/DELETE
)

class ArticleViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticatedOrReadOnly]

Custom Permissions

# myapp/permissions.py
from rest_framework.permissions import BasePermission

class IsAuthorOrReadOnly(BasePermission):
    """Allow authors to edit their own articles. Everyone else gets read-only."""

    def has_object_permission(self, request, view, obj):
        # Safe methods (GET, HEAD, OPTIONS) are always allowed
        if request.method in ("GET", "HEAD", "OPTIONS"):
            return True
        # Write permissions only for the article author
        return obj.author == request.user

class IsEditorOrAdmin(BasePermission):
    """Only editors and admins can access this endpoint."""

    def has_permission(self, request, view):
        return request.user.groups.filter(
            name__in=["editors", "admins"]
        ).exists()

# Usage in views
class ArticleViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]

    def get_permissions(self):
        if self.action == "create":
            return [IsAuthenticated()]
        if self.action in ("update", "partial_update", "destroy"):
            return [IsAuthenticated(), IsAuthorOrReadOnly()]
        return [AllowAny()]

10. Filtering, Searching, and Ordering

Install django-filter for powerful filtering:

pip install django-filter

# settings.py
INSTALLED_APPS = [..., "django_filters"]

REST_FRAMEWORK = {
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework.filters.SearchFilter",
        "rest_framework.filters.OrderingFilter",
    ],
}

Add filtering to your views:

from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]

    # Exact-match filtering: ?status=published&author=5
    filterset_fields = ["status", "author"]

    # Text search: ?search=django rest
    search_fields = ["title", "body", "author__username"]

    # Ordering: ?ordering=-created_at or ?ordering=title
    ordering_fields = ["created_at", "updated_at", "title"]
    ordering = ["-created_at"]  # default ordering

Custom FilterSet

import django_filters
from myapp.models import Article

class ArticleFilterSet(django_filters.FilterSet):
    title = django_filters.CharFilter(lookup_expr="icontains")
    created_after = django_filters.DateFilter(field_name="created_at", lookup_expr="gte")
    created_before = django_filters.DateFilter(field_name="created_at", lookup_expr="lte")
    author_name = django_filters.CharFilter(
        field_name="author__username", lookup_expr="icontains"
    )

    class Meta:
        model = Article
        fields = ["status", "title", "created_after", "created_before", "author_name"]

class ArticleViewSet(viewsets.ModelViewSet):
    filterset_class = ArticleFilterSet
    # ?title=django&status=published&created_after=2026-01-01

11. Pagination

DRF provides three built-in pagination styles:

# settings.py -- global default
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}

# Custom pagination classes
from rest_framework.pagination import (
    PageNumberPagination,
    LimitOffsetPagination,
    CursorPagination,
)

# Page number: ?page=2
class StandardPagination(PageNumberPagination):
    page_size = 20
    page_size_query_param = "page_size"  # ?page_size=50
    max_page_size = 100

# Limit/Offset: ?limit=20&offset=40
class LimitOffsetPagination(LimitOffsetPagination):
    default_limit = 20
    max_limit = 100

# Cursor (best for real-time feeds, no count query):
class ArticleCursorPagination(CursorPagination):
    page_size = 20
    ordering = "-created_at"
    cursor_query_param = "cursor"

# Apply per-view
class ArticleViewSet(viewsets.ModelViewSet):
    pagination_class = StandardPagination

Page number pagination returns count, next, previous, and results. Cursor pagination is the most performant for large datasets because it does not require a COUNT query and prevents clients from jumping to arbitrary pages (which can cause inconsistent results with real-time data).

12. Throttling

Throttling limits how many requests a client can make in a given time period. It protects your API from abuse and ensures fair usage:

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/hour",     # unauthenticated users
        "user": "1000/hour",    # authenticated users
    },
}

Custom Throttle Scopes

from rest_framework.throttling import ScopedRateThrottle

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": ["rest_framework.throttling.ScopedRateThrottle"],
    "DEFAULT_THROTTLE_RATES": {
        "uploads": "10/hour",
        "search": "60/minute",
        "contact": "5/day",
    },
}

# views.py
class FileUploadView(APIView):
    throttle_scope = "uploads"  # max 10 uploads per hour

class SearchView(APIView):
    throttle_scope = "search"   # max 60 searches per minute

13. Testing APIs

DRF provides APIClient and APITestCase built on Django's test framework. Use them to test your endpoints with proper authentication and content negotiation:

# myapp/tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from rest_framework.test import APIClient
from rest_framework import status
from myapp.models import Article

class ArticleAPITests(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.user = User.objects.create_user(
            username="testuser", password="testpass123"
        )
        self.article = Article.objects.create(
            title="Test Article",
            slug="test-article",
            body="Test body content that is long enough.",
            author=self.user,
            status="published",
        )

    def test_list_articles(self):
        response = self.client.get("/api/articles/")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data["results"]), 1)

    def test_create_article_authenticated(self):
        self.client.force_authenticate(user=self.user)
        data = {
            "title": "New Article",
            "slug": "new-article",
            "body": "Content for the new article with enough length.",
            "status": "draft",
        }
        response = self.client.post("/api/articles/", data, format="json")
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Article.objects.count(), 2)
        self.assertEqual(response.data["author"], self.user.id)

    def test_create_article_unauthenticated(self):
        data = {"title": "Fail", "slug": "fail", "body": "No auth", "status": "draft"}
        response = self.client.post("/api/articles/", data, format="json")
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

    def test_update_own_article(self):
        self.client.force_authenticate(user=self.user)
        response = self.client.patch(
            f"/api/articles/{self.article.slug}/",
            {"title": "Updated Title"},
            format="json",
        )
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.article.refresh_from_db()
        self.assertEqual(self.article.title, "Updated Title")

    def test_delete_other_users_article(self):
        other_user = User.objects.create_user("other", password="pass123")
        self.client.force_authenticate(user=other_user)
        response = self.client.delete(f"/api/articles/{self.article.slug}/")
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

For JWT-authenticated tests:

from rest_framework_simplejwt.tokens import RefreshToken

class JWTArticleTests(TestCase):
    def setUp(self):
        self.client = APIClient()
        self.user = User.objects.create_user("alice", password="secret")
        refresh = RefreshToken.for_user(self.user)
        self.client.credentials(
            HTTP_AUTHORIZATION=f"Bearer {refresh.access_token}"
        )

    def test_authenticated_request(self):
        response = self.client.get("/api/articles/")
        self.assertEqual(response.status_code, 200)

Run tests with python manage.py test myapp or use pytest with pytest-django for more flexibility.

14. Best Practices

Never use fields = "__all__" in serializers. Always explicitly list fields. Using __all__ exposes every model field, including sensitive ones you add later. Explicit field lists are self-documenting and prevent accidental data leaks.

Use separate serializers for read and write. Your list endpoint might return nested author data (read), but your create endpoint only needs an author ID (write). Use get_serializer_class() on your ViewSet to return different serializers based on the action.

Version your API. Use URL versioning (/api/v1/articles/) or header versioning. DRF supports both. Plan for versioning from day one because adding it later is painful:

# settings.py
REST_FRAMEWORK = {
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
    "ALLOWED_VERSIONS": ["v1", "v2"],
    "DEFAULT_VERSION": "v1",
}

# urls.py
urlpatterns = [
    path("api/v1/", include("myapp.urls_v1")),
    path("api/v2/", include("myapp.urls_v2")),
]

Select related objects in querysets. N+1 queries are the most common DRF performance issue. If your serializer accesses article.author.username, every article in a list triggers a separate database query for the author. Fix it with select_related and prefetch_related:

class ArticleViewSet(viewsets.ModelViewSet):
    def get_queryset(self):
        return Article.objects.select_related("author").prefetch_related("tags")

Handle errors consistently. DRF's default exception handler returns structured error responses. Customize it for a consistent API error format:

# myapp/exceptions.py
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):
    response = exception_handler(exc, context)
    if response is not None:
        response.data = {
            "error": True,
            "status_code": response.status_code,
            "detail": response.data,
        }
    return response

# settings.py
REST_FRAMEWORK = {
    "EXCEPTION_HANDLER": "myapp.exceptions.custom_exception_handler",
}

Generate API documentation. Use drf-spectacular for OpenAPI 3.0 schema generation. It produces Swagger UI and ReDoc documentation automatically:

# pip install drf-spectacular
# settings.py
INSTALLED_APPS = [..., "drf_spectacular"]
REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

urlpatterns = [
    path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
    path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema")),
]

Use environment variables for secrets. Never hardcode SECRET_KEY, database credentials, or API keys. Use django-environ or python-decouple and keep secrets in environment variables or a .env file excluded from version control.

Frequently Asked Questions

What is Django REST Framework and why should I use it?

Django REST Framework (DRF) is a powerful toolkit for building Web APIs on top of Django. It provides serializers for converting complex data types to JSON, class-based views and viewsets for handling HTTP methods, automatic URL routing, built-in authentication (Token, Session, JWT via third-party), permissions, filtering, pagination, and throttling. DRF saves months of development compared to building API infrastructure from scratch and is the standard choice for Django API projects.

What is the difference between APIView, GenericAPIView, and ViewSet?

APIView is the base class that maps HTTP methods (get, post, put, delete) to handler methods, giving you full control. GenericAPIView adds queryset and serializer_class plus mixin classes for common patterns. ViewSet combines multiple related views into a single class with actions like list, create, retrieve, update, and destroy, and works with routers for automatic URL configuration. Use APIView for custom logic, GenericAPIView with mixins for standard CRUD with some customization, and ModelViewSet for rapid full-CRUD endpoints.

How do I implement JWT authentication in Django REST Framework?

Install djangorestframework-simplejwt, add JWTAuthentication to DEFAULT_AUTHENTICATION_CLASSES in your DRF settings, and include the token obtain and refresh URL patterns. Clients POST credentials to /api/token/ to receive access and refresh tokens, then include Authorization: Bearer <access_token> in subsequent requests. Configure token lifetimes via SIMPLE_JWT in your settings. See the full Authentication section for working code examples.

How do serializers work in Django REST Framework?

Serializers convert complex data types like Django model instances into Python dictionaries that DRF renders as JSON. They also handle deserialization: parsing incoming JSON, validating it, and converting it back to model instances. ModelSerializer auto-generates fields from a Django model. You define which fields to include, add custom validation methods (validate_<field> or validate), and can nest serializers for related objects. Call serializer.is_valid() before accessing validated_data or save(). See the full Serializers section for details.

How do I add filtering, searching, and ordering to DRF endpoints?

Install django-filter and add DjangoFilterBackend, SearchFilter, and OrderingFilter to your filter backends. Set filterset_fields for exact-match filtering (?status=published), search_fields for text search (?search=django), and ordering_fields for sorting (?ordering=-created_at). For complex filtering, create a custom FilterSet class with range lookups and related field filtering. See the full Filtering section for code examples.

Related Resources