EthicsComputerCourses
Join training +91 90532 10052

Django REST Framework Intro

Django & Flask · Lesson 24 of 42 Advanced 7 min read Updated

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

Terminal
python -m pip install djangorestframework

Add "rest_framework" to INSTALLED_APPS, then set project-wide defaults:

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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
Python
# 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:

Output
{
  "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:

Output
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:

ClassAllows
AllowAnyEveryone
IsAuthenticatedOnly logged-in users
IsAuthenticatedOrReadOnlyEveryone reads; logged-in users write
IsAdminUserOnly users with is_staff
DjangoModelPermissionsUsers 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_name reads book.author for every row; use select_related("author") in get_queryset().
  • Forgetting basename when a viewset has no queryset attribute: the router raises an error.
  • Relying on IsAuthenticated for everything. Any logged-in user can then change any row; use model or object permissions.
  • Returning Django's JsonResponse from DRF views. Use Response so content negotiation and the browsable API work.

Key points

  • DRF adds serializers, API views, viewsets, routers, authentication, permissions and pagination to Django.
  • A ModelSerializer converts models to JSON and validates input; use source= for related data and read_only_fields for protected fields.
  • @api_view suits small endpoints; ModelViewSet plus a router gives full CRUD URLs.
  • Set defaults in REST_FRAMEWORK; choose permission classes such as IsAuthenticatedOrReadOnly or DjangoModelPermissions.
  • 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
Python
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)
Output
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?

2. A client sends a value for a field listed in read_only_fields. What happens?

3. Which permission class lets anyone read but only logged-in users write?

4. What does a DefaultRouter do with a registered ModelViewSet?

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