DRF Tutorial#

Django REST Framework (DRF) extends Django’s capabilities to easily build RESTful APIs. It provides serialization, authentication, viewsets, and many other features that make API development efficient and maintainable.

👉 New to App-Generator? Sign IN with GitHub or Generate Web Apps in no time (free service).

Key Features#

  • Powerful serialization system

  • Class-based views and viewsets

  • Authentication and permissions

  • Browsable API

  • Pagination, filtering, and searching

  • Extensive documentation

Installation and Setup#

Let’s start by installing DRF and setting it up in a Django project:

# Install Django and Django REST framework
pip install django
pip install djangorestframework

# Create a new Django project
django-admin startproject myproject
cd myproject

# Create an app
python manage.py startapp api

Update your settings.py to include the REST framework:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api',
]

# REST Framework settings
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

Models#

Let’s create a simple model in our app:

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

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=100)
    isbn = models.CharField(max_length=13, unique=True)
    published_date = models.DateField()

    def __str__(self):
        return self.title

After defining your model, run migrations:

python manage.py makemigrations
python manage.py migrate

Serializers#

Serializers allow complex data like querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML, or other content types. They also provide deserialization, allowing parsed data to be converted back into complex types.

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

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

    def validate_isbn(self, value):
        """
        Check that the ISBN is a valid 13-digit number
        """
        if len(value) != 13 or not value.isdigit():
            raise serializers.ValidationError("ISBN must be a 13-digit number")
        return value

Serializer Types#

  1. ModelSerializer: Automatically generates fields based on the model

  2. HyperlinkedModelSerializer: Uses hyperlinks for relationships

  3. Serializer: Base class for more custom serialization

Views#

DRF provides several types of views for handling API requests.

Function-based views#

# api/views.py
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Book
from .serializers import BookSerializer

@api_view(['GET', 'POST'])
def book_list(request):
    """
    List all books, or create a new book.
    """
    if request.method == 'GET':
        books = Book.objects.all()
        serializer = BookSerializer(books, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = BookSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view(['GET', 'PUT', 'DELETE'])
def book_detail(request, pk):
    """
    Retrieve, update or delete a book instance.
    """
    try:
        book = Book.objects.get(pk=pk)
    except Book.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = BookSerializer(book)
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = BookSerializer(book, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        book.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Class-based views#

# api/views.py (class-based version)
from rest_framework import generics
from .models import Book
from .serializers import BookSerializer

class BookList(generics.ListCreateAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

class BookDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

ViewSets#

ViewSets combine the logic for multiple related views into a single class:

# api/views.py (viewset version)
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    """
    This viewset automatically provides `list`, `create`, `retrieve`,
    `update` and `destroy` actions.
    """
    queryset = Book.objects.all()
    serializer_class = BookSerializer

URL Routing#

For function-based and class-based views:

# api/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from api import views

urlpatterns = [
    path('books/', views.BookList.as_view()),
    path('books/<int:pk>/', views.BookDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

For ViewSets:

# api/urls.py (for viewsets)
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from api import views

router = DefaultRouter()
router.register(r'books', views.BookViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Don’t forget to include your app URLs in the project:

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
    path('api-auth/', include('rest_framework.urls')),
]

Authentication and Permissions#

DRF provides a powerful system for authentication and permissions:

Authentication#

# settings.py excerpt
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ],
}

For token authentication, add ‘rest_framework.authtoken’ to INSTALLED_APPS and run migrations.

Permissions#

# api/views.py
from rest_framework import permissions

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

    def get_permissions(self):
        """
        Instantiates and returns the list of permissions that this view requires.
        """
        if self.action == 'list':
            permission_classes = [permissions.IsAuthenticated]
        else:
            permission_classes = [permissions.IsAdminUser]
        return [permission() for permission in permission_classes]

You can also create custom permissions:

# api/permissions.py
from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to edit it.
    """
    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner
        return obj.owner == request.user

Filtering and Pagination#

Filtering#

# api/views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ['author', 'published_date']
    search_fields = ['title', 'author']
    ordering_fields = ['published_date', 'title']

You’ll need to install django-filter:

pip install django-filter

And add it to your INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    'django_filters',
]

REST_FRAMEWORK = {
    # ...
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}

Pagination#

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
}

You can also create custom pagination classes:

# api/pagination.py
from rest_framework.pagination import PageNumberPagination

class LargeResultsSetPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'page_size'
    max_page_size = 1000

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = 'page_size'
    max_page_size = 100

Then apply it to your view:

# api/views.py
from .pagination import StandardResultsSetPagination

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

Nested Resources#

Let’s add a new model for book reviews:

# api/models.py
class Review(models.Model):
    book = models.ForeignKey(Book, related_name='reviews', on_delete=models.CASCADE)
    reviewer = models.CharField(max_length=100)
    content = models.TextField()
    rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)])
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']

Add a serializer for the review model and update the book serializer:

# api/serializers.py
class ReviewSerializer(serializers.ModelSerializer):
    class Meta:
        model = Review
        fields = ['id', 'reviewer', 'content', 'rating', 'created_at']

class BookSerializer(serializers.ModelSerializer):
    reviews = ReviewSerializer(many=True, read_only=True)

    class Meta:
        model = Book
        fields = ['id', 'title', 'author', 'isbn', 'published_date', 'reviews']

Create a nested view for reviews:

# api/views.py
class ReviewViewSet(viewsets.ModelViewSet):
    serializer_class = ReviewSerializer

    def get_queryset(self):
        return Review.objects.filter(book_id=self.kwargs['book_pk'])

    def perform_create(self, serializer):
        serializer.save(book_id=self.kwargs['book_pk'])

Update the URL configuration for nested resources:

# api/urls.py
from rest_framework_nested import routers
from .views import BookViewSet, ReviewViewSet

router = routers.DefaultRouter()
router.register(r'books', BookViewSet)

books_router = routers.NestedDefaultRouter(router, r'books', lookup='book')
books_router.register(r'reviews', ReviewViewSet, basename='book-reviews')

urlpatterns = [
    path('', include(router.urls)),
    path('', include(books_router.urls)),
]

You’ll need to install the DRF nested routers package:

pip install drf-nested-routers

Versioning#

DRF provides API versioning to help you manage changes to your API over time:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    'DEFAULT_VERSION': 'v1',
    'ALLOWED_VERSIONS': ['v1', 'v2'],
    'VERSION_PARAM': 'version',
}

Update your URLs:

# myproject/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/<str:version>/', include('api.urls')),
    path('api-auth/', include('rest_framework.urls')),
]

Then in your views:

# api/views.py
class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()

    def get_serializer_class(self):
        if self.request.version == 'v1':
            return BookSerializerV1
        return BookSerializerV2

Testing#

DRF provides testing utilities that extend Django’s existing test framework:

# api/tests.py
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Book

class BookTests(APITestCase):
    def test_create_book(self):
        """
        Ensure we can create a new book object.
        """
        url = reverse('book-list')
        data = {
            'title': 'Django REST Framework',
            'author': 'Django Team',
            'isbn': '9781234567890',
            'published_date': '2020-01-01'
        }
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Book.objects.count(), 1)
        self.assertEqual(Book.objects.get().title, 'Django REST Framework')

    def test_get_books(self):
        """
        Ensure we can retrieve the book list.
        """
        Book.objects.create(
            title='Django REST Framework',
            author='Django Team',
            isbn='9781234567890',
            published_date='2020-01-01'
        )
        url = reverse('book-list')
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)

Throttling#

Throttling is used to control the rate of requests that clients can make to your API:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}

For specific views:

# api/views.py
from rest_framework.throttling import UserRateThrottle

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

Content Negotiation#

DRF supports rendering responses in multiple formats:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
        'rest_framework.renderers.XMLRenderer',
    ],
}

Conclusion#

Django REST Framework is a comprehensive toolkit that simplifies the process of building RESTful APIs. It provides powerful features like serialization, authentication, viewsets, and more, all while maintaining the ease of use that Django is known for.

This tutorial covered the basics to get you started, but DRF offers much more. Check the official documentation for advanced features and best practices.