Building APIs with Django REST Framework
Django itself renders HTML. Django REST Framework (DRF) — a separate, near-universal third-party package — is what turns models into JSON APIs, using the same serializer/view vocabulary that maps directly onto Forms and CBVs you already know.
4 min read
What DRF actually is, and why it's a separate package
Django's core HttpResponse/render() machinery is built around returning HTML. Django REST Framework (djangorestframework on PyPI, imported as rest_framework) is not part of Django itself — it's a separate, third-party package, but one so widely adopted that it's the de facto standard for building JSON APIs on top of Django, to the point that "Django API" and "DRF" are used almost interchangeably in practice. It adds Serializers (the API equivalent of Forms), API-specific views, and browsable API tooling — all designed to feel familiar to anyone who already knows Django's Forms and class-based views, because they deliberately mirror that vocabulary.
Serializers: converting model instances to and from JSON
from rest_framework import serializers
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ["id", "title", "body", "published_at"]A ModelSerializer is the direct API analog of a ModelForm from the forms lesson: it generates fields automatically from a model, and handles both directions — turning a model instance into JSON-serializable data (serializer.data), and turning incoming JSON into a validated model instance (serializer.is_valid(), serializer.save()), the same is_valid()/cleaned_data shape Django forms already use, just aimed at JSON instead of HTML form data.
article = Article.objects.get(pk=1)
serializer = ArticleSerializer(article)
serializer.data # {'id': 1, 'title': 'Hello', 'body': '...', 'published_at': '2024-01-15T...'}API views: the CBV pattern, aimed at JSON instead of HTML
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
class ArticleListCreateView(ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
class ArticleDetailView(RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializerThis is structurally identical to the generic CBVs from the class-based views lesson — ListCreateAPIView and RetrieveUpdateDestroyAPIView are the API equivalents of ListView/CreateView and DetailView/UpdateView/DeleteView, configured the same way (a queryset and a serializer instead of a template). ListCreateAPIView handles GET (list all) and POST (create one); RetrieveUpdateDestroyAPIView handles GET (one object), PUT/PATCH (update), and DELETE — the same "generic view implements a common shape, subclass configures it" idea, just producing JSON responses instead of rendered templates.
ViewSet and the router: even less boilerplate
from rest_framework.viewsets import ModelViewSet
from rest_framework.routers import DefaultRouter
class ArticleViewSet(ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
router = DefaultRouter()
router.register("articles", ArticleViewSet)
# generates: GET/POST /articles/, GET/PUT/PATCH/DELETE /articles/<pk>/ — automaticallyA ModelViewSet bundles list, create, retrieve, update, and destroy into one class, and DefaultRouter automatically generates the URL patterns for all of them from a single router.register() call — no manual path() entries needed for any of the five operations. This trades some of the explicit visibility a plain CBV gives you (the exact URL patterns aren't written out anywhere) for genuinely less code once a model needs the full standard CRUD set of endpoints, which is extremely common for API resources.
Real API responses need real HTTP status codes
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
class ArticleCreateView(APIView):
def post(self, request):
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)Unlike a browser-facing view (which mostly returns 200 or redirects), an API consumer — often another program, not a human — needs to distinguish "created successfully" (201) from "validation failed" (400) from "not found" (404) programmatically, since that's how client code decides what to do next. rest_framework.status provides named constants (HTTP_201_CREATED instead of the bare number 201) specifically so this intent is readable in the code, not just correct by coincidence.
Serializer validation: the same idea as Form validation, restated for JSON
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ["id", "title", "body"]
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Title must be at least 5 characters.")
return valuevalidate_<field_name> on a serializer is the exact same idea as a custom clean_<field_name> method on a Django Form — field-specific validation logic that runs during is_valid(), raising a validation error that ends up in serializer.errors if the check fails. Anyone who already understands Django Form validation from the forms lesson already understands the shape of this — DRF deliberately reuses it rather than inventing a separate validation vocabulary.
Why this matters: DRF is the seam between a Django backend and everything else
A Django app rendering its own templates is a complete, self-contained web application. The moment a mobile app, a separate JavaScript frontend, or another service needs to read or write the same data, HTML responses stop being useful — those consumers need structured JSON they can parse programmatically, which is exactly the gap DRF fills. This is also why DRF becomes relevant the moment a project's frontend and backend are meant to be genuinely decoupled (a React or mobile app calling a Django backend purely as an API), rather than Django rendering the UI itself.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Is Django REST Framework part of Django's core, or a separate package?
2. How does a ModelSerializer relate to a ModelForm from the forms lesson?
3. What does registering a ModelViewSet with a DefaultRouter generate automatically?
4. Why does an API view need to return specific status codes like 201 or 400 instead of just 200 for everything?