Django REST Framework Intro
In A Small Flask REST API you wrote every piece of an API by hand: converting objects to dictionaries, checking JSON types, choosing status codes, turning errors into JSON. For Django, almost everyone uses Django REST Framework (DRF), a third-party package that does all of this and adds authentication, permissions, pagination and a browsable web interface for your API.
In this lesson you will expose the Jaipur City Library's books and authors as a JSON API. You will write serializers that convert models to JSON and validate input, build endpoints with @api_view and viewsets, connect them with a router, and control access with permissions and pagination.
Install and configure
python -m pip install djangorestframeworkAdd "rest_framework" to INSTALLED_APPS, then set project-wide defaults:
# config/settings.py
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticatedOrReadOnly"],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 3,
}IsAuthenticatedOrReadOnly lets anyone read (GET) but requires a logged-in user to create, change or delete. PageNumberPagination splits lists into pages of three, which is small but makes paging easy to see.
Serializers
A serializer converts model objects into Python data that becomes JSON, and in the other direction, validates incoming JSON before it becomes a model. ModelSerializer builds its fields from the model, much like a ModelForm:
# catalog/serializers.py
from rest_framework import serializers
from .models import Author, Book
class BookSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.name", read_only=True)
class Meta:
model = Book
fields = ["id", "title", "slug", "author", "author_name", "language", "year", "price", "available"]
read_only_fields = ["available"]
def validate_year(self, value):
if value < 1800:
raise serializers.ValidationError("We only stock books from 1800 onwards.")
return value
class AuthorSerializer(serializers.ModelSerializer):
book_count = serializers.IntegerField(source="books.count", read_only=True)
class Meta:
model = Author
fields = ["id", "name", "born", "book_count"]source="author.name" follows the relationship to show the author's name next to the author ID. read_only_fields lets clients see available but never set it. validate_<field>() works like clean_<field>() in forms. As with ModelForms, always list fields explicitly so new model fields are not exposed by accident.
Function views with @api_view
For a simple endpoint, decorate a function with @api_view and return a DRF Response:
# catalog/api.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Author, Book
@api_view(["GET"])
def library_stats(request):
return Response({"books": Book.objects.count(), "authors": Author.objects.count()})Response takes ordinary Python data and renders it as JSON (or as an HTML page in the browser). @api_view also rejects other methods with 405 and parses incoming JSON into request.data.
ViewSets and routers
For a full set of list, detail, create, update and delete endpoints, a ModelViewSet does it in a few lines, and a router generates the URLs:
# catalog/api.py (continued)
from rest_framework import viewsets
from .serializers import AuthorSerializer, BookSerializer
class BookViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
lookup_field = "slug"
def get_queryset(self):
books = Book.objects.select_related("author")
language = self.request.query_params.get("language")
if language:
books = books.filter(language=language)
return books
class AuthorViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Author.objects.order_by("name")
serializer_class = AuthorSerializer# catalog/api_urls.py
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from . import api
router = DefaultRouter()
router.register("books", api.BookViewSet, basename="book")
router.register("authors", api.AuthorViewSet)
urlpatterns = [
path("stats/", api.library_stats),
path("", include(router.urls)),
]
# config/urls.py: path("api/", include("catalog.api_urls")),The router creates /api/books/ (list and create), /api/books/<slug>/ (read, update, partial update, delete), the same for authors (read-only), and an API root page at /api/. lookup_field = "slug" puts the slug in URLs instead of the ID. Because BookViewSet uses get_queryset() instead of a queryset attribute, the router needs basename="book" to name its URLs.
Trying the API
Reading needs no login. Filtering with ?language=bn returns:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 3,
"title": "Gitanjali",
"slug": "gitanjali",
"author": 2,
"author_name": "Rabindranath Tagore",
"language": "bn",
"year": 1910,
"price": "199.00",
"available": 4
}
]
}The pagination wrapper adds count, next and previous. Decimal prices are sent as strings ("199.00") so no precision is lost. Here is a session with DRF's test client, APIClient, showing status codes and bodies:
GET /api/stats/ 200 {"books": 7, "authors": 5}
GET /api/books/ 200 {"count": 7, "next": "http://testserver/api/books/?page=2", ...}
POST /api/books/ (anonymous) 403 {"detail": "Authentication credentials were not provided."}
POST /api/books/ (priya, year 1750) 400 {"year": ["We only stock books from 1800 onwards."]}
POST /api/books/ (priya, available 99) 201 {"id": 8, "title": "Pinjar", ..., "price": "220.00", "available": 0}
PATCH /api/books/pinjar/ {"price": ...} 200 {..., "price": "199.00", "available": 0}
DELETE /api/books/pinjar/ 204
GET /api/authors/?page=2 200 {"count": 5, "next": null, "previous": "http://testserver/api/authors/", "results": [...]}Notice the 201 response: the client sent "available": 99, but the field is read-only, so it was ignored and the default 0 was saved. Validation errors come back as 400 with a message per field, exactly like form errors.
Open http://127.0.0.1:8000/api/books/ in a browser and DRF shows the browsable API: an HTML page with the JSON, links to follow, and forms for POST and PATCH when you are logged in. It is a great tool while developing and for teammates exploring your API.
Authentication and permissions
DRF checks authentication (who is calling) and then permissions (are they allowed). By default it accepts Django's session login (used by the browsable API) and HTTP Basic. Mobile apps usually send a token instead: DRF includes TokenAuthentication (rest_framework.authtoken), and many projects use JSON Web Tokens through a separate package.
Useful permission classes:
| Class | Allows |
|---|---|
AllowAny | Everyone |
IsAuthenticated | Only logged-in users |
IsAuthenticatedOrReadOnly | Everyone reads; logged-in users write |
IsAdminUser | Only users with is_staff |
DjangoModelPermissions | Users with the matching add/change/delete model permission |
In the session above, Priya could create and delete books although her group only has change_book, because IsAuthenticatedOrReadOnly checks login, not model permissions. For the library you would set permission_classes = [DjangoModelPermissions] on BookViewSet, so the permissions you set up in Authentication & Permissions apply to the API too.
Common mistakes
fields = "__all__"in serializers. Every future model field becomes public and writable; list fields explicitly.- N+1 queries in serializers.
author_namereadsbook.authorfor every row; useselect_related("author")inget_queryset(). - Forgetting
basenamewhen a viewset has noquerysetattribute: the router raises an error. - Relying on
IsAuthenticatedfor everything. Any logged-in user can then change any row; use model or object permissions. - Returning Django's
JsonResponsefrom DRF views. UseResponseso content negotiation and the browsable API work.
Key points
- DRF adds serializers, API views, viewsets, routers, authentication, permissions and pagination to Django.
- A
ModelSerializerconverts models to JSON and validates input; usesource=for related data andread_only_fieldsfor protected fields. @api_viewsuits small endpoints;ModelViewSetplus a router gives full CRUD URLs.- Set defaults in
REST_FRAMEWORK; choose permission classes such asIsAuthenticatedOrReadOnlyorDjangoModelPermissions. - Errors are returned as JSON with proper status codes (400, 403, 404), and the browsable API helps during development.
Exercise
Add a read-only endpoint /api/books/<slug>/similar/ that returns up to three other books in the same language as the given book, using the @action decorator on BookViewSet. Return the serialized books in a JSON list (no pagination).
Show solution
from rest_framework.decorators import action
from rest_framework.response import Response
class BookViewSet(viewsets.ModelViewSet):
# ... as before
@action(detail=True, methods=["get"])
def similar(self, request, slug=None):
book = self.get_object()
others = (Book.objects.select_related("author")
.filter(language=book.language).exclude(pk=book.pk)[:3])
return Response(self.get_serializer(others, many=True).data)GET /api/books/godaan/similar/ -> 200 [{"id": 2, "title": "Nirmala", "slug": "nirmala", "author": 1, "author_name": "Munshi Premchand", "language": "hi", "year": 1927, "price": "180.00", "available": 0}]@action(detail=True) adds an extra URL under each book, and the router registers it automatically. self.get_object() finds the book by slug (and returns 404 if missing), and many=True serializes a list.
Quick Quiz
1. What does a DRF serializer do?
Serializers work in both directions, like a ModelForm for APIs.
2. A client sends a value for a field listed in read_only_fields. What happens?
Read-only fields appear in output but are skipped on input.
3. Which permission class lets anyone read but only logged-in users write?
Safe methods (GET, HEAD, OPTIONS) are open; others need authentication.
4. What does a DefaultRouter do with a registered ModelViewSet?
Routers map viewset actions to URLs such as /books/ and /books/<lookup>/.
Learn Django & Flask with a mentor
Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
