Skip to main content

Django REST Views

In this tutorial, we'll explore the various view implementations available in Django REST Framework (DRF) for building powerful APIs. Views are a crucial component in any REST API as they handle HTTP requests and return appropriate responses.

Introduction to DRF Views

Views in Django REST Framework are responsible for processing incoming HTTP requests and returning HTTP responses. They encapsulate the logic for:

  • Parsing request data
  • Validating the data using serializers
  • Performing database operations
  • Formatting and returning responses

DRF provides several ways to implement views, ranging from function-based views to highly abstracted viewsets. The right choice depends on your specific requirements and how much abstraction you need.

Types of Views in DRF

Let's explore the main types of views available in Django REST Framework:

  1. Function-based views
  2. Class-based views (APIView)
  3. Generic views
  4. ViewSets and Routers

Function-Based Views

Function-based views are the simplest way to create API endpoints. They're ideal when you need complete control over the request handling logic.

How to Create Function-Based Views

First, import the necessary modules:

python
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Book
from .serializers import BookSerializer

Then, create a function-based view:

python
@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)

The @api_view decorator specifies which HTTP methods the view should respond to.

Example Request/Response

For a GET request to /api/books/:

Response (200 OK):

json
[
{
"id": 1,
"title": "Django for Beginners",
"author": "William S. Vincent",
"published_date": "2020-01-01"
},
{
"id": 2,
"title": "Django for APIs",
"author": "William S. Vincent",
"published_date": "2020-06-01"
}
]

For a POST request to /api/books/:

Request Body:

json
{
"title": "Django for Professionals",
"author": "William S. Vincent",
"published_date": "2021-01-01"
}

Response (201 Created):

json
{
"id": 3,
"title": "Django for Professionals",
"author": "William S. Vincent",
"published_date": "2021-01-01"
}

Class-Based Views (APIView)

Class-based views provide a more organized way to handle different HTTP methods. DRF's APIView class is a subclass of Django's View class with some additional features.

How to Create a Class-Based View

python
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Book
from .serializers import BookSerializer

class BookList(APIView):
"""
List all books, or create a new book.
"""
def get(self, request):
books = Book.objects.all()
serializer = BookSerializer(books, many=True)
return Response(serializer.data)

def post(self, request):
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)

class BookDetail(APIView):
"""
Retrieve, update or delete a book instance.
"""
def get_object(self, pk):
try:
return Book.objects.get(pk=pk)
except Book.DoesNotExist:
raise Http404

def get(self, request, pk):
book = self.get_object(pk)
serializer = BookSerializer(book)
return Response(serializer.data)

def put(self, request, pk):
book = self.get_object(pk)
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)

def delete(self, request, pk):
book = self.get_object(pk)
book.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

To wire these up in your urls.py:

python
from django.urls import path
from . import views

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

Generic Class-Based Views

Generic views provide pre-implemented behavior for common use cases. They help reduce boilerplate code and keep your views clean.

Common Generic Views

  1. ListAPIView: For read-only endpoints that represent a collection of model instances
  2. CreateAPIView: For endpoints that create model instances
  3. RetrieveAPIView: For read-only endpoints that represent a single model instance
  4. UpdateAPIView: For endpoints that update model instances
  5. DestroyAPIView: For endpoints that delete model instances
  6. ListCreateAPIView: For read-write endpoints that represent a collection of model instances
  7. RetrieveUpdateDestroyAPIView: For read-write-delete endpoints that represent a single model instance

Here's how to implement the same book API using generic views:

python
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

That's it! Just two simple classes to handle all CRUD operations. DRF's generic views handle the implementation details for you.

Customizing Generic Views

You can customize generic views by overriding their methods:

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

def get_queryset(self):
"""
Optionally restricts the returned books,
by filtering against a `genre` query parameter in the URL.
"""
queryset = Book.objects.all()
genre = self.request.query_params.get('genre', None)
if genre is not None:
queryset = queryset.filter(genre=genre)
return queryset

def perform_create(self, serializer):
"""
Associate the current user with the book being created
"""
serializer.save(owner=self.request.user)

ViewSets and Routers

ViewSets and routers represent the highest level of abstraction in DRF. They are particularly useful when you want to perform standard CRUD operations.

Creating a ViewSet

python
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing book instances.
"""
queryset = Book.objects.all()
serializer_class = BookSerializer

The ModelViewSet class automatically provides .list(), .retrieve(), .create(), .update(), .partial_update(), and .destroy() actions.

Setting Up Routers

Routers automatically generate URL patterns for your ViewSets:

python
from rest_framework.routers import DefaultRouter
from .views import BookViewSet

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

urlpatterns = router.urls

This router will generate the following URL patterns:

  • GET /books/ - list all books
  • POST /books/ - create a new book
  • GET /books/{pk}/ - retrieve a specific book
  • PUT /books/{pk}/ - update a specific book
  • PATCH /books/{pk}/ - partially update a specific book
  • DELETE /books/{pk}/ - delete a specific book

Practical Example: Building a Blog API

Let's consolidate our knowledge by building a simplified blog API. We'll implement endpoints for posts and comments.

First, create the models:

python
# models.py
from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)

def __str__(self):
return self.title

class Comment(models.Model):
post = models.ForeignKey(Post, related_name='comments', on_delete=models.CASCADE)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)

def __str__(self):
return f'Comment by {self.author.username} on {self.post.title}'

Next, create serializers:

python
# serializers.py
from rest_framework import serializers
from .models import Post, Comment
from django.contrib.auth.models import User

class CommentSerializer(serializers.ModelSerializer):
author = serializers.ReadOnlyField(source='author.username')

class Meta:
model = Comment
fields = ['id', 'text', 'created_at', 'author', 'post']

class PostSerializer(serializers.ModelSerializer):
author = serializers.ReadOnlyField(source='author.username')
comments = CommentSerializer(many=True, read_only=True)

class Meta:
model = Post
fields = ['id', 'title', 'content', 'created_at', 'author', 'comments']

Now, implement the ViewSets:

python
# views.py
from rest_framework import viewsets, permissions
from .models import Post, Comment
from .serializers import PostSerializer, CommentSerializer

class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer

def perform_create(self, serializer):
serializer.save(author=self.request.user)

class CommentViewSet(viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer

def perform_create(self, serializer):
serializer.save(author=self.request.user)

Finally, configure the URLs using a router:

python
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r'posts', views.PostViewSet)
router.register(r'comments', views.CommentViewSet)

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

This gives us a complete API with endpoints for both posts and comments, with minimal code.

Summary

Django REST Framework offers a spectrum of view implementations, from simple function-based views to highly abstracted ViewSets:

  1. Function-based views: Best for simple use cases or custom behavior
  2. Class-based views (APIView): More structured approach for handling HTTP methods
  3. Generic views: Pre-built functionality for common patterns
  4. ViewSets and Routers: Highest level of abstraction, ideal for standard CRUD operations

When choosing which type to use, consider:

  • The complexity of your API
  • How much customization you need
  • How closely your endpoints align with standard REST patterns
  • How much code duplication you're willing to accept

Generally, it's best to use the most abstracted option that still meets your needs, but don't hesitate to mix different types in the same project where appropriate.

Additional Resources

Exercises

  1. Create a function-based view that returns a list of all books published in a specific year (passed as a query parameter).
  2. Implement a class-based view for a Category model that returns all books in that category.
  3. Create a generic view that lists all books by a specific author.
  4. Implement a ViewSet for an Author model with custom actions to:
    • List top-rated authors
    • Get the total number of books written by an author
  5. Extend the blog API example to include user authentication and ensure users can only edit their own posts and comments.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)