Part 6, Django REST Framework basics
Install
pip install djangorestframeworkAdd "rest_framework" to INSTALLED_APPS and at minimum configure defaults:
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": 25,}Serializers, the ORM ↔ JSON boundary
A serializer turns model instances into JSON (and validated JSON back into model instances).
from rest_framework import serializersfrom .models import Post, Tag
class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag fields = ["id", "name"]
class PostSerializer(serializers.ModelSerializer): author = serializers.ReadOnlyField(source="author.username") tags = TagSerializer(many=True, read_only=True) tag_ids = serializers.PrimaryKeyRelatedField( queryset=Tag.objects.all(), many=True, write_only=True, source="tags", )
class Meta: model = Post fields = ["id", "author", "title", "slug", "body", "tags", "tag_ids", "published_at"] read_only_fields = ["id", "slug", "published_at"]Two patterns shown:
ReadOnlyField(source="author.username"), flatten a nested attribute.- Separate read/write for relations, read nested (
tags), write by PK (tag_ids). Cleaner than nested write, which DRF deliberately makes awkward.
ViewSets and routers
from rest_framework import viewsets, permissionsfrom rest_framework.decorators import actionfrom rest_framework.response import Responsefrom .models import Postfrom .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.select_related("author").prefetch_related("tags") serializer_class = PostSerializer
def get_permissions(self): if self.action in {"list", "retrieve"}: return [permissions.AllowAny()] return [permissions.IsAuthenticated()]
def perform_create(self, serializer): serializer.save(author=self.request.user)
@action(detail=True, methods=["post"]) def publish(self, request, pk=None): from django.utils import timezone post = self.get_object() post.published_at = timezone.now() post.save() return Response({"status": "published"})Wire the router:
from django.urls import include, pathfrom rest_framework.routers import DefaultRouterfrom blog.api_views import PostViewSet
router = DefaultRouter()router.register(r"posts", PostViewSet, basename="post")
urlpatterns = [ path("api/", include(router.urls)),]That registers:
| Method | URL | Action |
|---|---|---|
| GET | /api/posts/ | list |
| POST | /api/posts/ | create |
| GET | /api/posts/{pk}/ | retrieve |
| PUT | /api/posts/{pk}/ | update |
| PATCH | /api/posts/{pk}/ | partial_update |
| DELETE | /api/posts/{pk}/ | destroy |
| POST | /api/posts/{pk}/publish/ | custom @action |
Authentication choices
- SessionAuthentication, browser sessions; pairs with Django auth, respects CSRF.
- TokenAuthentication, a long-lived opaque token stored in the DB, sent as
Authorization: Token <key>. - JWT (via
djangorestframework-simplejwt), short-lived access tokens, long-lived refresh tokens, no DB lookup per request.
For mobile/SPA backends, JWT with simplejwt is the common choice.
Permissions
Built-in classes:
AllowAny,IsAuthenticated,IsAdminUser,IsAuthenticatedOrReadOnly.DjangoModelPermissions, maps HTTP methods to Django’sadd_/change_/delete_/view_permissions.DjangoObjectPermissions, object-level (requires a backend like guardian).
Custom permission:
class IsAuthorOrReadOnly(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True return obj.author == request.userFiltering, searching, ordering
pip install django-filterfrom django_filters.rest_framework import DjangoFilterBackendfrom rest_framework import filters
class PostViewSet(viewsets.ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields = ["author", "published_at"] search_fields = ["title", "body"] ordering_fields = ["published_at", "title"]Now /api/posts/?author=3&search=django&ordering=-published_at works out of the box.
Pagination
PageNumberPagination (shown above) returns:
{ "count": 317, "next": "http://api.example.com/posts/?page=3", "previous": "http://api.example.com/posts/?page=1", "results": [ ... ]}For large lists, consider CursorPagination, uses an opaque cursor, stable under concurrent writes.
Nested and related data
The serializer example above does lists via select_related / prefetch_related in the viewset. This matters, the default DRF pattern N+1’s the database fast:
# BAD: N+1 query on every postqueryset = Post.objects.all()
# GOOD: one query for posts + one for authors + one for tagsqueryset = Post.objects.select_related("author").prefetch_related("tags")Part 7 dives deeper.
OpenAPI / schema generation
DRF ships a schema generator. For full OpenAPI with nice UI:
pip install drf-spectacularINSTALLED_APPS += ["drf_spectacular"]REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"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"), name="docs"),]You get an interactive Swagger UI at /api/docs/ generated from your serializers and viewsets.
Gotchas
ModelSerializer.create()does not write M2M relations if you passcommit=Falsethrough it, useperform_createto handle.- Nested writes are intentionally painful. DRF doesn’t want you mutating multiple tables through one endpoint. Either use split read/write fields (above) or write
create()/update()methods explicitly. - Browsable API in production, DRF’s default HTML-rendered browsable API is a dev luxury; disable in production by removing
BrowsableAPIRendererfromDEFAULT_RENDERER_CLASSES. - Throttling, DRF has
AnonRateThrottleandUserRateThrottlebut they’re opt-in. For serious protection, use nginx/Cloudflare rate limiting in addition. - Versioning, decide your scheme (URL path, header, query param) before shipping. Changing later is painful for clients.
What’s next
Part 7 goes deep on the ORM, select_related, prefetch_related, Q/F, subqueries, aggregation.
References
- Django REST Framework docs
- drf-spectacular, OpenAPI schema generator
- django-filter, filter backend
- django-rest-framework-simplejwt, JWT auth