Skip to content

Introduction to Django REST Framework: Building Modern APIs in Python

Published: at 10:00 AMSuggest Changes

Django REST Framework (DRF) is the de facto standard for building REST APIs in Django. It takes everything you already love about Django —the ORM, the admin, the security defaults— and adds a powerful, batteries-included toolkit for designing APIs. In this post we will go from zero to a working API, covering serializers, views, authentication, permissions, pagination, and the best practices that separate a toy project from a production-ready service.

Table of Contents

Open Table of Contents

What is Django REST Framework?

Django REST Framework is a third-party package built on top of Django that provides a flexible and expressive way to build Web APIs. While Django itself can serve JSON responses, DRF abstracts the repetitive parts —parsing, validation, serialization, content negotiation, authentication— so you can focus on your domain logic.

Some of the reasons DRF has become the default choice in the Django ecosystem:

Prerequisites

Before starting, you should be comfortable with:

Installation and Project Setup

Start by creating a virtual environment and installing Django and DRF:

python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

pip install django djangorestframework

Now bootstrap a new Django project and an app to hold our API:

django-admin startproject config .
python manage.py startapp books

Register both rest_framework and the new app in config/settings.py:

# config/settings.py
INSTALLED_APPS = [
    # ... default apps
    "rest_framework",
    "books",
]

Run the initial migrations to make sure everything is wired up:

python manage.py migrate

Defining a Model

Let’s model a simple Book resource. Add the following to books/models.py:

# books/models.py
from django.db import models


class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    published_year = models.PositiveIntegerField()
    isbn = models.CharField(max_length=13, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self) -> str:
        return f"{self.title} ({self.author})"

Create and apply the migration:

python manage.py makemigrations
python manage.py migrate

Serializers: The Heart of DRF

A serializer translates between complex Python objects (like Django model instances) and primitive data types that can be rendered as JSON. It also handles validation of incoming data.

The most common base class is ModelSerializer, which inspects the model to generate fields automatically:

# books/serializers.py
from rest_framework import serializers
from .models import Book


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ["id", "title", "author", "published_year", "isbn", "created_at"]
        read_only_fields = ["id", "created_at"]

Custom Validation

You often need domain-specific rules. DRF offers field-level and object-level validation:

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ["id", "title", "author", "published_year", "isbn", "created_at"]

    def validate_isbn(self, value: str) -> str:
        if len(value) not in (10, 13):
            raise serializers.ValidationError("ISBN must be 10 or 13 characters.")
        return value

    def validate(self, data: dict) -> dict:
        if data["published_year"] > 2026:
            raise serializers.ValidationError("published_year cannot be in the future.")
        return data

Views and ViewSets

DRF offers several layers of abstraction for writing views. From lowest to highest:

  1. APIView: request/response style, similar to Django’s class-based views.
  2. Generic views (ListCreateAPIView, RetrieveUpdateDestroyAPIView, etc.): CRUD on top of a queryset and serializer.
  3. ViewSets: group related actions (list, create, retrieve, update, destroy) into a single class that plugs into a router.

For a typical CRUD resource, a ModelViewSet is the shortest path:

# books/views.py
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer


class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

That single class exposes GET /books/, POST /books/, GET /books/{id}/, PUT /books/{id}/, PATCH /books/{id}/, and DELETE /books/{id}/.

Routers and URL Configuration

Routers wire ViewSets to URLs automatically:

# books/urls.py
from rest_framework.routers import DefaultRouter
from .views import BookViewSet

router = DefaultRouter()
router.register(r"books", BookViewSet, basename="book")

urlpatterns = router.urls

Then include the app URLs in the project:

# config/urls.py
from django.contrib import admin
from django.urls import path, include

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

Start the development server and you will have a fully browsable API at http://127.0.0.1:8000/api/books/:

python manage.py runserver

Authentication

DRF supports multiple authentication schemes. The most common in production are Token Authentication and JWT.

Token Authentication

Enable token auth globally in settings:

# config/settings.py
INSTALLED_APPS += ["rest_framework.authtoken"]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
}

Run python manage.py migrate to create the token table. You can then expose an endpoint to obtain tokens:

# config/urls.py
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns += [
    path("api/auth/token/", obtain_auth_token),
]

Clients send the token in the Authorization header:

Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

JWT Authentication

For stateless, scalable APIs, JWT is usually preferred. The djangorestframework-simplejwt package is the community standard:

pip install djangorestframework-simplejwt
# config/settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}
# config/urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns += [
    path("api/auth/jwt/", TokenObtainPairView.as_view()),
    path("api/auth/jwt/refresh/", TokenRefreshView.as_view()),
]

Permissions

Authentication answers who are you?; permissions answer what are you allowed to do?. DRF ships with common classes:

Set a default globally and override per view when needed:

# config/settings.py
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}
# books/views.py
from rest_framework.permissions import IsAuthenticatedOrReadOnly


class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

For fine-grained rules (for example, only the owner can edit), write a custom permission:

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj) -> bool:
        if request.method in permissions.SAFE_METHODS:
            return True
        return obj.owner == request.user

Production APIs almost always need pagination. Enable it globally:

# config/settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}

For filtering and search, install and configure django-filter:

pip install django-filter
# config/settings.py
INSTALLED_APPS += ["django_filters"]

REST_FRAMEWORK = {
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework.filters.SearchFilter",
        "rest_framework.filters.OrderingFilter",
    ],
}
# books/views.py
class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    filterset_fields = ["author", "published_year"]
    search_fields = ["title", "author"]
    ordering_fields = ["published_year", "created_at"]

Clients can now hit endpoints like /api/books/?author=Tolkien&search=ring&ordering=-published_year.

Testing Your API

DRF provides an APIClient that makes writing tests straightforward:

# books/tests.py
from django.contrib.auth import get_user_model
from rest_framework.test import APITestCase
from rest_framework import status
from .models import Book


class BookAPITests(APITestCase):
    def setUp(self) -> None:
        self.user = get_user_model().objects.create_user(
            username="reader", password="secret123"
        )
        self.client.force_authenticate(user=self.user)

    def test_create_book(self) -> None:
        payload = {
            "title": "The Hobbit",
            "author": "J.R.R. Tolkien",
            "published_year": 1937,
            "isbn": "9780547928227",
        }
        response = self.client.post("/api/books/", payload, format="json")
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Book.objects.count(), 1)

Run the test suite with:

python manage.py test

Best Practices

A few habits that pay dividends as your API grows:

When to Use DRF (and When Not To)

DRF is an excellent choice when:

It may be overkill if:

Conclusion

Django REST Framework remains the most productive way to build REST APIs in the Python ecosystem. It layers serialization, validation, authentication, permissions, and pagination on top of Django’s solid foundations, and it scales from weekend projects to services handling millions of requests. Start with ModelViewSet and routers to get moving quickly, then reach for custom permissions, filters, and throttles as your API matures. Pair it with drf-spectacular for documentation and djangorestframework-simplejwt for stateless authentication, and you have a stack that will serve you well for years.


Next Post
Gaussian Splatting vs. Traditional Photogrammetry