FroquizFroquiz
HomeQuizzesSenior ChallengeGet CertifiedBlogAbout
Sign InStart Quiz
Sign InStart Quiz
Froquiz

The most comprehensive quiz platform for software engineers. Test yourself with 10000+ questions and advance your career.

LinkedIn

Platform

  • Start Quizzes
  • Topics
  • Blog
  • My Profile
  • Sign In

About

  • About Us
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

Β© 2026 Froquiz. All rights reserved.Built with passion for technology
Blog & Articles

Django REST Framework: Building APIs with Python and DRF

Learn how to build production APIs with Django REST Framework. Covers serializers, views, viewsets, authentication, permissions, filtering, pagination, and testing.

Yusuf SeyitoğluMarch 12, 20262 views11 min read

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

bash
pip 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

python
from 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

python
from 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:

python
from 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/ β€” list
  • POST /api/posts/ β€” create
  • GET /api/posts/{id}/ β€” detail
  • PUT/PATCH /api/posts/{id}/ β€” update
  • DELETE /api/posts/{id}/ β€” delete
  • POST /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

bash
pip 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:

code
GET /api/posts/?status=published&search=django&ordering=-created_at

Testing 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

  • ModelSerializer generates fields from models automatically β€” use it for model-backed resources
  • ViewSets combine all CRUD operations; DefaultRouter generates URLs automatically
  • perform_create is the right place to inject author=request.user before saving
  • Custom permissions via BasePermission β€” has_permission for view-level, has_object_permission for object-level
  • django-filter provides URL-based filtering, search, and ordering with minimal code
  • Always use select_related and prefetch_related in ViewSet querysets to avoid N+1 queries
  • APITestCase with force_authenticate makes testing authenticated endpoints straightforward

About Author

Yusuf Seyitoğlu

Author β†’

Other Posts

  • CSS Advanced Techniques: Custom Properties, Container Queries, Grid Masonry and Modern LayoutsMar 12
  • Java Collections Deep Dive: ArrayList, HashMap, TreeMap, LinkedHashMap and When to Use EachMar 12
  • GraphQL Schema Design: Types, Resolvers, Mutations and Best PracticesMar 12
All Blogs