Django REST Framework: Exposing an API Layer

Food Tracker (Django)

Chapter 11 · Django REST Framework: Exposing an API Layer

Chapter 8 deliberately avoided DRF for one simple search endpoint. By now there are four genuinely JSON-shaped features — search, add-item, mark-used, recipe lookup — plus Chapter 5's own full-page-reload cost still unresolved. That's the actual threshold this chapter waits for.

Installing DRF

pip install djangorestframework
INSTALLED_APPS = [ ..., "rest_framework", "pantry", ]

A Serializer Isn't a Second ModelForm

from rest_framework import serializers from .models import Item class ItemSerializer(serializers.ModelSerializer): class Meta: model = Item fields = ["id", "name", "barcode", "category", "status", "expiry_date", "added_at", "used_at"] read_only_fields = ["status", "added_at", "used_at"]

Like Chapter 6's ModelForm, ModelSerializer derives its fields directly from the same Item model — no separate schema class to duplicate. But it's built for JSON in, JSON out, with no HTML rendering assumption at all. And read_only_fields works differently from Chapter 6's own commit=False pattern: commit=False lets the view set server-controlled fields after the form validates but before saving; read_only_fields instead tells the serializer itself to include a field in output (a GET response) while silently ignoring any attempt to set it via input — a genuinely different mechanism arriving at a similarly-shaped safety outcome, not the identical technique wearing a new name.

ViewSets and Routers: The Pattern Repeats, One Level Up

from rest_framework import viewsets from .models import Item from .serializers import ItemSerializer class ItemViewSet(viewsets.ModelViewSet): queryset = Item.objects.all() serializer_class = ItemSerializer
# urls.py from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register("items", ItemViewSet) urlpatterns = [path("api/", include(router.urls))]

One ViewSet class, registered with a router, auto-generates a full set of RESTful endpoints — list, create, retrieve, update, delete — with no individual view functions written by hand for any of them. It's Django's own "batteries included" philosophy repeating itself one level deeper, this time inside DRF specifically.

The Honest Hybrid Shape

This chapter doesn't throw away Chapters 3 through 10 and replace everything with DRF. The admin (Chapter 3) stays exactly what it was. The dashboard and history pages could keep their server-rendered templates for the parts that don't need deep interactivity, while search, add-item, mark-used, and recipe lookup can genuinely migrate to these new /api/ endpoints, called via fetch() instead of a full-page form POST or redirect. Some pages stay templates; some features become a real JSON API. This hybrid shape is the honest, common reality most substantial Django applications actually settle into — not a failure to fully commit to one philosophy.

Chapter 8's own reasoning, resolved the other way
Chapter 8 declined DRF because one endpoint didn't justify it. This chapter reaches for it precisely because there are now enough genuinely interactive features, sharing enough real serialization and validation needs, that the earlier cost/benefit calculation flips. Neither decision was wrong — they were the correct answer at two different points in the same app's own growth.
The browsable API is worth seeing directly
Visiting /api/items/ in an ordinary browser renders a genuinely pleasant, interactive HTML exploration UI for the JSON API itself — automatically, with zero extra code required to get it.
Switching to JSON doesn't switch off CSRF protection
Migrating Chapter 5's scan-and-redirect flow to instead fetch() a DRF endpoint directly from the browser doesn't exempt that call from Django's own CSRF protection — a same-origin, session-authenticated browser request still needs the CSRF token included in the request's own headers for any unsafe method (POST/PUT/DELETE). Forgetting it produces the exact same 403 rejection Chapter 6 already covered for a template form, just now from JavaScript instead of an HTML <form>.

Where This Course Is Headed

Deployment, and a capstone tying every chapter together into one complete, working app.

Hands-On Exercises

Exercise 1

Explain why this chapter is genuinely the right point to introduce DRF, tying your answer back to Chapter 8's own reasoning for avoiding it. What specifically changed between Chapter 8 and now?

📄 View solution
Exercise 2

Explain the difference between Chapter 6's commit=False pattern and this chapter's read_only_fields. Both prevent user control of a field — how do they actually achieve that differently?

📄 View solution
Exercise 3

Explain why switching Chapter 5's scan flow from a template form POST to a fetch() call against a DRF endpoint doesn't bypass Django's CSRF protection, and what would happen if the CSRF token were left out of that fetch() call.

📄 View solution

Chapter 11 Quick Reference

  • ModelSerializer — derives from the same model as ModelForm, but for JSON, not HTML
  • read_only_fields vs. commit=False — genuinely different mechanisms, similar safety outcome
  • ViewSet + router — one class auto-generates a full RESTful endpoint set
  • The hybrid shape — templates and a JSON API coexisting is the honest norm, not a compromise
  • CSRF still applies to same-origin fetch() calls — the token must be sent explicitly in headers
  • Next chapter: Deployment