Django REST Framework: Building APIs with Python and DRF
Django REST Framework (DRF) is the most popular library for building REST APIs with Python. It provides serializers for data validation and transformation, class-based views for rapid development, and a browsable API interface that makes development and debugging enjoyable. This guide walks through building a real API from scratch.
Setup
bashpip install django djangorestframework djangorestframework-simplejwt
python# settings.py INSTALLED_APPS = [ ... "rest_framework", "rest_framework_simplejwt", "myapp", ] 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": 20, }
Models
python# models.py from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): bio = models.TextField(blank=True) avatar_url = models.URLField(blank=True) class Category(models.Model): name = models.CharField(max_length=100) slug = models.SlugField(unique=True) def __str__(self): return self.name class Post(models.Model): class Status(models.TextChoices): DRAFT = "draft", "Draft" PUBLISHED = "published", "Published" title = models.CharField(max_length=200) content = models.TextField() author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts") category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True) status = models.CharField(max_length=20, choices=Status.choices, default=Status.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
Serializers
Serializers handle conversion between Python objects and JSON, plus input validation:
python# serializers.py from rest_framework import serializers from .models import Post, Category, User class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ["id", "username", "email", "bio", "avatar_url"] read_only_fields = ["id"] class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = ["id", "name", "slug"] class PostListSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) category_name = serializers.CharField(source="category.name", read_only=True) class Meta: model = Post fields = ["id", "title", "author", "category_name", "status", "created_at"] class PostDetailSerializer(serializers.ModelSerializer): author = UserSerializer(read_only=True) category = CategorySerializer(read_only=True) category_id = serializers.PrimaryKeyRelatedField( queryset=Category.objects.all(), source="category", write_only=True, required=False, ) class Meta: model = Post fields = [ "id", "title", "content", "author", "category", "category_id", "status", "created_at", "updated_at", ] read_only_fields = ["id", "author", "created_at", "updated_at"] def validate_title(self, value): if len(value) < 5: raise serializers.ValidationError("Title must be at least 5 characters.") return value def validate(self, attrs): -- Cross-field validation if attrs.get("status") == "published" and not attrs.get("content"): raise serializers.ValidationError( {"content": "Content is required before publishing."} ) return attrs
Views
Function-based views
pythonfrom rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from rest_framework import status @api_view(["GET"]) @permission_classes([AllowAny]) def health_check(request): return Response({"status": "ok"})
Class-based views
pythonfrom rest_framework.views import APIView class PostListView(APIView): permission_classes = [IsAuthenticated] def get(self, request): posts = Post.objects.select_related("author", "category").all() serializer = PostListSerializer(posts, many=True) return Response(serializer.data) def post(self, request): serializer = PostDetailSerializer(data=request.data) if serializer.is_valid(): serializer.save(author=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
ViewSets (the DRF way)
ViewSets combine list, create, retrieve, update, and delete into one class:
pythonfrom rest_framework import viewsets, permissions from rest_framework.decorators import action from rest_framework.response import Response class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.select_related("author", "category").all() permission_classes = [permissions.IsAuthenticatedOrReadOnly] def get_serializer_class(self): if self.action == "list": return PostListSerializer return PostDetailSerializer def get_queryset(self): queryset = super().get_queryset() status_filter = self.request.query_params.get("status") author_id = self.request.query_params.get("author") if status_filter: queryset = queryset.filter(status=status_filter) if author_id: queryset = queryset.filter(author_id=author_id) return queryset def perform_create(self, serializer): serializer.save(author=self.request.user) def perform_update(self, serializer): if self.get_object().author != self.request.user: raise PermissionDenied("You can only edit your own posts.") serializer.save() @action(detail=True, methods=["post"], url_path="publish") def publish(self, request, pk=None): post = self.get_object() if post.author != request.user: return Response( {"error": "Not allowed"}, status=status.HTTP_403_FORBIDDEN ) post.status = Post.Status.PUBLISHED post.save() return Response(PostDetailSerializer(post).data) @action(detail=False, methods=["get"], url_path="my-posts") def my_posts(self, request): posts = self.get_queryset().filter(author=request.user) serializer = self.get_serializer(posts, many=True) return Response(serializer.data)
URL Configuration
python# urls.py from rest_framework.routers import DefaultRouter from django.urls import path, include from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from . import views router = DefaultRouter() router.register("posts", views.PostViewSet) router.register("categories", views.CategoryViewSet) urlpatterns = [ path("api/", include(router.urls)), path("api/auth/token/", TokenObtainPairView.as_view()), path("api/auth/token/refresh/", TokenRefreshView.as_view()), ]
The router automatically generates URLs:
GET /api/posts/β listPOST /api/posts/β createGET /api/posts/{id}/β detailPUT/PATCH /api/posts/{id}/β updateDELETE /api/posts/{id}/β deletePOST /api/posts/{id}/publish/β custom action
Custom Permissions
python# permissions.py from rest_framework.permissions import BasePermission, SAFE_METHODS class IsAuthorOrReadOnly(BasePermission): """Allow read to anyone; writes only to the resource author.""" def has_object_permission(self, request, view, obj): if request.method in SAFE_METHODS: return True return obj.author == request.user class IsAdminOrReadOnly(BasePermission): def has_permission(self, request, view): if request.method in SAFE_METHODS: return True return request.user and request.user.is_staff
Filtering, Search, and Ordering
bashpip install django-filter
python# settings.py REST_FRAMEWORK = { ... "DEFAULT_FILTER_BACKENDS": [ "django_filters.rest_framework.DjangoFilterBackend", "rest_framework.filters.SearchFilter", "rest_framework.filters.OrderingFilter", ], } # views.py import django_filters class PostFilter(django_filters.FilterSet): created_after = django_filters.DateFilter(field_name="created_at", lookup_expr="gte") created_before = django_filters.DateFilter(field_name="created_at", lookup_expr="lte") class Meta: model = Post fields = ["status", "category", "author"] class PostViewSet(viewsets.ModelViewSet): filterset_class = PostFilter search_fields = ["title", "content"] ordering_fields = ["created_at", "title"] ordering = ["-created_at"]
Now clients can filter:
GET /api/posts/?status=published&search=django&ordering=-created_atTesting DRF APIs
python# tests.py from rest_framework.test import APITestCase, APIClient from rest_framework import status from django.contrib.auth import get_user_model User = get_user_model() class PostAPITest(APITestCase): def setUp(self): self.user = User.objects.create_user( username="alice", email="alice@example.com", password="testpass123" ) self.client.force_authenticate(user=self.user) def test_create_post(self): data = {"title": "Test Post Title", "content": "Some content here"} response = self.client.post("/api/posts/", data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data["title"], "Test Post Title") self.assertEqual(response.data["author"]["username"], "alice") def test_unauthenticated_cannot_create(self): self.client.force_authenticate(user=None) response = self.client.post("/api/posts/", {"title": "x", "content": "y"}) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_list_posts_returns_published_only(self): Post.objects.create(title="Draft", content="...", author=self.user, status="draft") Post.objects.create(title="Live", content="...", author=self.user, status="published") response = self.client.get("/api/posts/?status=published") self.assertEqual(len(response.data["results"]), 1) self.assertEqual(response.data["results"][0]["title"], "Live")
Common Interview Questions
Q: What is the difference between a Serializer and a ModelSerializer?
Serializer is the base class β you define all fields manually. ModelSerializer automatically generates fields from a Django model, reducing boilerplate significantly. It also generates default create() and update() methods. Use ModelSerializer for model-backed resources; use Serializer for non-model data like login forms.
Q: How does DRF handle authentication vs authorization?
Authentication identifies who the user is β DRF checks the Authorization header and runs configured authentication classes (JWT, session, token). Authorization determines what the authenticated user can do β permission classes on views and has_object_permission on individual objects.
Q: What is the difference between perform_create and overriding create?
Both let you customize object creation. perform_create(serializer) is a hook specifically for adding data before saving β like setting author=request.user. Overriding create(request) gives you full control of the response. For simple field injection, perform_create is cleaner and the DRF convention.
Practice Python on Froquiz
Django and DRF knowledge is tested in Python backend interviews. Test your Python knowledge on Froquiz β covering OOP, async, decorators, and more.
Summary
ModelSerializergenerates fields from models automatically β use it for model-backed resources- ViewSets combine all CRUD operations;
DefaultRoutergenerates URLs automatically perform_createis the right place to injectauthor=request.userbefore saving- Custom permissions via
BasePermissionβhas_permissionfor view-level,has_object_permissionfor object-level django-filterprovides URL-based filtering, search, and ordering with minimal code- Always use
select_relatedandprefetch_relatedin ViewSet querysets to avoid N+1 queries APITestCasewithforce_authenticatemakes testing authenticated endpoints straightforward