~/TechPurAI
~/tutorials/django-rest-framework/class-based-apiview
intermediate·part 7 of 22·3 min read

Class-based views: APIView

Updated Aug 31, 2026Python · Django

@api_view branches on request.method inside one function — fine for two methods, harder to read as a resource grows more operations. APIView is DRF's class-based alternative: one method per HTTP verb, the same shape Django's own class-based View uses, with DRF's request/response/content-negotiation layered in.

Rewriting the list-and-create view

python
# tasks/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Task
from .serializers import TaskSerializer

class TaskListView(APIView):
    def get(self, request):
        tasks = Task.objects.all()
        serializer = TaskSerializer(tasks, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = TaskSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save(owner=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Same logic as part 4's @api_view function, split into get() and post() methods instead of an if request.method == branch. APIView automatically returns 405 Method Not Allowed for any HTTP method without a matching method defined on the class — the same protection @api_view's method list gave for free.

A detail view: single-task operations

python
from rest_framework.generics import get_object_or_404

class TaskDetailView(APIView):
    def get(self, request, pk):
        task = get_object_or_404(Task, pk=pk)
        serializer = TaskSerializer(task)
        return Response(serializer.data)

    def put(self, request, pk):
        task = get_object_or_404(Task, pk=pk)
        serializer = TaskSerializer(task, 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):
        task = get_object_or_404(Task, pk=pk)
        task.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

get_object_or_404 here is DRF's own version, imported from rest_framework.generics rather than django.shortcuts — functionally identical, but DRF's raises Http404 in a way DRF's exception handling (part 11 touches on this) converts into a proper JSON 404 response instead of Django's default HTML error page, which would be the wrong content type for an API entirely.

Wiring up both

python
# tasks/urls.py
urlpatterns = [
    path("tasks/", views.TaskListView.as_view(), name="task-list"),
    path("tasks/<int:pk>/", views.TaskDetailView.as_view(), name="task-detail"),
]

.as_view() — same requirement as every class-based view in the Django from scratch series, DRF's APIView included. <int:pk> captures the task's ID from the URL and passes it to every method on TaskDetailView as the pk keyword argument.

APIView or @api_view?

Functionally interchangeable for simple cases — @api_view stays more compact for one or two methods; APIView reads more clearly once there's a get, post, put, and delete all doing meaningfully different things. Neither is what most real DRF projects reach for directly, though — part 8's generic views remove almost all the repetition still visible in TaskDetailView above.

FAQ

What's the difference between APIView and a ViewSet? A ViewSet (not covered until later in this series) groups the list, create, retrieve, update, and delete logic for one resource into a single class with conventionally-named methods, paired with a router that generates the URL patterns automatically. APIView requires writing both the class and the urls.py entries by hand, as done here — more explicit, more boilerplate.

Where would permission checks (like "only the task's owner can delete it") go? DRF's permission_classes attribute on the view, checked before the method body runs — not covered in this part, since it's introduced once authentication itself is set up later in the series. Right now, any authenticated user can operate on any task.

Why does post() call serializer.save(owner=request.user) but put() just calls serializer.save()? Creating a task needs an owner assigned, and request.user provides it since owner isn't in the submitted form data. Updating an existing task already has an owner set from when it was created, so put()'s serializer.save() doesn't need to (and shouldn't) reassign it.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous6. Status codes done properly with rest_framework.statusnext →8. Generic views: ListCreateAPIView and RetrieveUpdateDestroyAPIView