~/TechPurAI
~/tutorials/django-rest-framework/function-based-api-views
beginner·part 4 of 22·2 min read

Function-based API views with @api_view

Updated Aug 16, 2026Python · Django

@api_view is DRF's simplest entry point for a view — a regular Django view function, wrapped to get DRF's request/response handling (part 5 covers exactly what that adds). This part builds the first real endpoint: list every task, and create a new one.

A list-and-create view

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

@api_view(["GET", "POST"])
def task_list(request):
    if request.method == "GET":
        tasks = Task.objects.all()
        serializer = TaskSerializer(tasks, many=True)
        return Response(serializer.data)

    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)

@api_view(["GET", "POST"]) declares which HTTP methods this view accepts — anything else gets an automatic 405 Method Not Allowed, with no branch of your own needed to produce that response. many=True on the GET branch tells the serializer it's working with a queryset (many objects), not a single instance — without it, DRF tries to serialize the queryset as if it were one Task and fails. serializer.save(owner=request.user) is how owner, marked read-only in part 3's Meta, actually gets set: not from the submitted data, but assigned explicitly by the view, which is exactly the point of making it read-only in the first place.

Wiring it up

python
# tasks/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("tasks/", views.task_list, name="task-list"),
]
python
# taskapi/urls.py
from django.urls import path, include

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

Same include() pattern as every URL setup in the Django from scratch series — nothing DRF-specific here, since routing itself is still plain Django.

Seeing it work

bash
python manage.py runserver

Visit http://127.0.0.1:8000/api/tasks/ in a browser and DRF's browsable API renders — a full HTML interface showing the JSON response, with a form at the bottom for submitting a POST directly from the page (part 18 covers this properly). The same URL from curl gets plain JSON instead:

bash
curl http://127.0.0.1:8000/api/tasks/
json
[{"id":1,"title":"Write the API","description":"","completed":false,"due_date":null,"owner":1,"created_at":"2026-08-16T10:00:00Z"}]

DRF decides which format to return based on the request's Accept header — a browser sends one that prefers HTML, curl by default sends one that's happy with anything, and DRF's content negotiation picks JSON. Same view, same serializer, two different rendered formats, automatically.

Common mistake

Forgetting many=True when serializing a queryset. The error — something like 'QuerySet' object has no attribute 'id' deep in DRF's internals — doesn't obviously point at the missing argument, so it's worth recognizing on sight: a queryset (or any list of objects) into a serializer always needs many=True.

Next: what request and Response actually are in DRF — genuinely different objects from Django's own HttpRequest/HttpResponse, not just aliases.

VK

Vijay Kumar

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

LinkedIn ↗
← previous3. ModelSerializer: the shortcut for model-backed APIsnext →5. Request and Response: DRF's upgrades over Django's own objects