Django REST Framework

Installing DRF, ModelSerializer, and a complete ViewSet + router example for a JSON API.

What Django REST Framework adds

Django's own views and forms are built for producing HTML. Django REST Framework (DRF) sits on top of the same models, ORM, and URL system and exists specifically for building JSON APIs — serialization, request parsing, authentication/permission hooks, browsable documentation, and class-based views tuned for REST-shaped resources (list, retrieve, create, update, destroy), instead of you assembling all of that by hand out of JsonResponse and manual validation.

It's the de facto standard for building an API on top of Django — if a Django project needs to expose any kind of JSON API, DRF is almost always the first tool reached for rather than a hand-rolled alternative.

Installing DRF

Bash
pip install djangorestframework
Python
# mysite/settings.py
INSTALLED_APPS = [
    # ...
    "rest_framework",
    "blog",
]

No migrations are needed just to install DRF itself — it layers on top of models you've already defined and migrated (like the Author and Post models from earlier in this track).

Serializers

A serializer is DRF's equivalent of a Django ModelForm for APIs: it describes how to convert a model instance to a JSON-compatible structure, and how to validate incoming JSON and turn it back into a model instance.

Python
# blog/serializers.py
from rest_framework import serializers
from .models import Author, Post

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

    class Meta:
        model = Post
        fields = ["id", "title", "body", "published", "created_at", "author", "author_name"]
        read_only_fields = ["id", "created_at"]

ModelSerializer derives its fields directly from the model, the same way ModelForm does — fields controls which of them the API exposes. source="author.name" lets a serializer field reach across a relationship and expose a derived, read-only value (the author's name) that isn't itself a column on Post.

Python
serializer = PostSerializer(post_instance)
serializer.data
# {'id': 1, 'title': 'Hello, Django', 'body': '...', 'published': True, 'created_at': '...', 'author': 1, 'author_name': 'Ada Lovelace'}

A complete ViewSet + router example

A ViewSet groups the standard list/retrieve/create/update/destroy behavior for one resource into a single class, instead of five separate views:

Python
# blog/views.py
from rest_framework import viewsets, permissions
from .models import Post
from .serializers import PostSerializer

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all().order_by("-created_at")
    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

    def get_queryset(self):
        if self.request.query_params.get("published") == "true":
            return self.queryset.filter(published=True)
        return self.queryset

ModelViewSet gives you list, retrieve, create, update, partial_update, and destroy for free from just queryset and serializer_classget_queryset() is overridden here to support an optional ?published=true filter, exactly the kind of one-method customization class-based views are good at.

A router then generates the full set of URL patterns for a ViewSet automatically:

Python
# blog/urls.py
from rest_framework.routers import DefaultRouter
from .views import PostViewSet

router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")

urlpatterns = router.urls
Python
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("blog.urls")),
]

router.register("posts", PostViewSet, basename="post") wires up GET/POST /api/posts/ and GET/PUT/PATCH/DELETE /api/posts/{id}/ in one line — the same set of conventional endpoints Route::resource() generates in Laravel, or a ListView+DetailView pair would in plain Django.

The browsable API

Visiting /api/posts/ in a browser (rather than calling it from curl or a JS client) shows DRF's browsable API — an auto-generated HTML interface listing the resource, letting you submit a form to POST a new one, and showing exactly what headers and status code came back. It's enormously useful for manual testing and for exploring an API during development, and it needs zero extra code — it comes from the same serializer and viewset already written above.

Common mistakes

  • Writing a plain serializers.Serializer with every field spelled out by hand when a ModelSerializer would derive them automatically from the model — extra code to keep in sync every time the model changes.
  • Forgetting permission_classes, leaving a viewset's default (often AllowAny, depending on project-wide DEFAULT_PERMISSION_CLASSES) wide open to writes from anyone.
  • Registering a ViewSet directly with path() instead of a router — it works for narrow cases via .as_view({'get': 'list'}), but throws away the automatic, conventional URL generation a router provides for the full set of actions.

Interview questions

Q: What does a DRF ModelSerializer actually do, and how does it compare to a Django ModelForm? It derives a set of fields, validation, and (de)serialization logic directly from a model, the same way ModelForm derives form fields from a model — the difference is a ModelSerializer converts between a model instance and JSON-compatible Python data (for an API), while ModelForm converts between a model instance and HTML form fields (for a server-rendered page). Both remove the need to hand-write field definitions that just repeat what the model already declares.

Q: What's the benefit of a ModelViewSet plus a router over writing individual APIView classes for each endpoint? A ModelViewSet bundles list/retrieve/create/update/destroy behavior for one resource into a single class derived from just queryset and serializer_class, and a router (like DefaultRouter) generates the full, conventional set of URLs for it automatically — GET/POST /posts/ and GET/PUT/PATCH/DELETE /posts/{id}/ — in one router.register() call. Writing separate APIView classes for each of those would mean repeating the same list/detail logic and manually wiring up each URL by hand.