Featured image of post Class-Based Views di Django REST Framework

Class-Based Views di Django REST Framework

Penjelasan mengenai Class-Based Views di DRF menggunakan APIView, HTTP method sebagai method class, permissions, dan authentication hooks.

Sebelumnya kita sudah membangun Task API secara lengkap dengan metode Function-Based Views (FBV). Kita menulis function Python biasa, men-decorate-nya dengan @api_view, dan menggunakan branching model if request.method == "GET" untuk menangani HTTP method yang berbeda-beda di dalam function yang sama.

Pendekatan tersebut bisa dilakukan, tapi ada masalah pada arsitektur kodenya. Semakin besar sebuah view (lebih banyak method, lebih banyak validasi, lebih banyak setup code yang dipakai bersama), satu function dengan rantai if statement yang panjang akan semakin sulit dibaca dan sulit digunakan ulang. Materi ini memperkenalkan tool berikutnya di DRF: Class-Based Views, khususnya class APIView.

Di akhir materi ini, kita akan membangun ulang Task API yang sama dengan yang sebelumnya ada di materi Function-Based Views, tapi kali ini menggunakan class, bukan function. Setelah menyelesaikan materi kali ini, kita akan benar-benar memahami kenapa DRF memberikan opsi ini.

Berikut ide dasarnya dalam satu kalimat: daripada menggunakan satu function yang bercabang berdasarkan request.method, kita menulis satu class di mana setiap HTTP method punya method-nya sendiri.

Bandingkan bentuk dari kedua pendekatan ini:

1
2
3
4
5
6
7
# Function-Based View (yang kita lakukan sebelumnya)
@api_view(["GET", "POST"])
def task_list(request):
    if request.method == "GET":
        ...
    if request.method == "POST":
        ...

dengan

1
2
3
4
5
6
7
# Class-Based View (yang kita pelajari sekarang)
class TaskList(APIView):
    def get(self, request):
        ...

    def post(self, request):
        ...

Perhatikan apa yang berubah. Daripada mengecek request.method menggunakan if statement, kita cukup menulis method bernama get dan method bernama post. DRF (melalui Django di baliknya) akan melihat method dari request yang masuk dan otomatis memanggil method yang sesuai pada class kita. Kalau client mengirim request GET, method get() kita yang akan berjalan. Kalau mereka mengirim POST, method post() kita yang akan berjalan. Kita tidak pernah perlu menulis if check itu sendiri.

Ini bukan konsep yang benar-benar baru. Ini adalah siklus request/response yang sama dengan yang kita pelajari di materi Function-Based Views (method, path, headers, body yang masuk; status code, headers, body yang keluar). Hanya cara kita mengorganisir kode kita saja yang berubah.


Kenapa kita membutuhkan ini?

Kita mungkin bertanya: “Kalau FBV sudah berfungsi, kenapa kita perlu belajar cara kedua untuk melakukan hal yang sama?”

Berikut alasan-alasan praktisnya:

Pemisahan logic yang lebih rapi

Dengan FBV, semua logic untuk setiap method berada di dalam satu function body, hanya dipisahkan oleh if statement. Dengan CBV, setiap method punya tempatnya sendiri. Tidak ada kemungkinan kita tidak sengaja menulis logic GET di dalam block yang seharusnya untuk POST, karena keduanya adalah method yang terpisah.

Setup code yang dipakai bersama

Bayangkan setiap method di sebuah view perlu mengecek sesuatu terlebih dahulu, misalnya, mengambil object tertentu dari database berdasarkan primary key-nya. Dengan FBV, kita harus mengulang logic lookup tersebut (atau block try/except-nya) di setiap if branch, atau menariknya keluar sebelum branch-branch tersebut dengan cara yang agak awkward. Dengan class, kita bisa mendefinisikan satu helper method saja (seperti get_object()) dan memanggilnya dari get(), put(), dan delete() tanpa perlu mengulang-ulang.

Inheritance dan reuse

Class bisa melakukan inherit dari class lainnya. Ini berarti kita bisa membangun view “dasar” dengan behavior yang umum dan membiarkan view lainnya melakukan extend terhadapnya. Kita belum terlalu banyak menggunakan ini di materi kali ini, tapi ini akan menjadi sangat powerful begitu kita sampai di Generic Views pada materi selanjutnya. APIView adalah dasar dari semua hal lainnya di sistem class-based milik DRF.

Permission dan authentication hook yang sudah tersedia

Ini adalah alasan yang signifikan, dan kita akan membahasnya secara khusus nanti di bagian bawah. APIView memberi kita class-level attribute (permission_classes, authentication_classes) yang terhubung langsung ke sistem authentication dan permission milik DRF. Dengan FBV, kita perlu decorator terpisah (@permission_classes, @authentication_classes) yang ditumpuk di atas @api_view. Dengan CBV, ini cukup menjadi sebuah attribute pada class-nya, yang menurut banyak developer lebih natural untuk dibaca dan di-maintain.

Ini bukan berarti FBV itu “salah”. FBV masih bagus untuk endpoint yang sangat kecil, sederhana, dan one-off. Tapi seiring backend API kita berkembang, kita akan semakin mengandalkan pattern class-based, jadi penting untuk membangun mental model ini dari sekarang.


Class APIView

APIView adalah class yang disediakan oleh DRF. Ini adalah versi class-based dari decorator @api_view. Saat kita menulis view class, kita melakukan inherit dari APIView:

1
2
3
4
5
6
7
from rest_framework.views import APIView
from rest_framework.response import Response


class HelloView(APIView):
    def get(self, request):
        return Response({"message": "Hello from a class-based view"})

Mari kita baca baris per baris.

from rest_framework.views import APIView

Perhatikan bahwa import path-nya berbeda dari @api_view. Decorator api_view berada di rest_framework.decorators. Class APIView berada di rest_framework.views. Ini mudah tertukar saat kita masih baru, jadi akan membantu kalau kita ingat: decorator untuk function berasal dari decorators, class berasal dari views.

class HelloView(APIView):

Ini adalah class Python biasa yang di-inherit dari APIView. Semua hal spesial soal bagaimana class ini berperilaku (parsing request.data, membungkus request ke dalam object Request milik DRF, menangani authentication, dan lain-lain) berasal dari apa yang di-inherit dari APIView. Kita tidak membangun mesin-nya. Kita hanya mencolok kebutuhan kita ke mesin tersebut.

def get(self, request):

Ini adalah instance method Python biasa. Penamaan get di sini tidak sembarangan. DRF (melalui class View milik Django di baliknya) melihat HTTP method dari request yang masuk, mengubahnya menjadi lowercase, lalu mencoba memanggil method dengan nama yang persis sama pada class kita. Request GET memanggil get(). Request POST memanggil post(). Request PUT memanggil put(). Request DELETE memanggil delete(). Request PATCH memanggil patch().

Perhatikan bahwa parameter pertamanya adalah self, karena ini adalah method pada sebuah class, bukan function yang berdiri sendiri. Parameter kedua adalah request, sama persis seperti di dunia FBV. Ini tetap object Request milik DRF yang sudah ditingkatkan, dengan request.data, request.query_params, dan request.user semuanya tersedia, persis seperti yang kita pelajari di materi Function-Based Views.

Apa yang terjadi kalau client mengirim method yang tidak kita definisikan?

Kalau client mengirim request DELETE ke HelloView, dan kita tidak pernah mendefinisikan method delete(), DRF akan otomatis merespon dengan 405 Method Not Allowed. Kita mendapatkan ini secara gratis, persis seperti perilaku whitelist method pada @api_view(["GET"]) di materi Function-Based Views. Kita tidak perlu menulis kode tambahan apapun untuk menolak method yang tidak didukung.


Implementasi get(), post(), put(), delete()

Mari kita lihat seperti apa view yang lebih lengkap, berdampingan dengan versi FBV dari materi Function-Based Views, supaya perbedaannya jelas terlihat.

List view (GET all, POST new)

Versi FBV (dari materi Function-Based Views):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@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)

    if request.method == "POST":
        serializer = TaskSerializer(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)

Versi CBV (yang sedang kita pelajari sekarang):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class TaskList(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()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Baca kedua kode ini berdampingan pelan-pelan dan teliti. Isi dari setiap block logic-nya hampir identik. Yang berubah hanya pembungkus luarnya: daripada satu function dengan if request.method == "GET", kita punya dua method, get dan post, dan if check-nya menghilang karena nama method itu sendiri yang memberi tahu DRF kapan harus menjalankannya.

Detail view (GET satu, PUT update, DELETE)

Versi FBV (dari materi Function-Based Views):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@api_view(["GET", "PUT", "DELETE"])
def task_detail(request, pk):
    try:
        task = Task.objects.get(pk=pk)
    except Task.DoesNotExist:
        return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)

    if request.method == "GET":
        serializer = TaskSerializer(task)
        return Response(serializer.data)

    if request.method == "PUT":
        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)

    if request.method == "DELETE":
        task.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Versi CBV:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TaskDetail(APIView):
    def get_object(self, pk):
        try:
            return Task.objects.get(pk=pk)
        except Task.DoesNotExist:
            raise Http404

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

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

Ini adalah momen di mana pendekatan class-based mulai terlihat kelebihannya. Perhatikan get_object(self, pk). Ini adalah helper method kecil yang kita tulis sendiri. Ini tidak diwajibkan oleh DRF, tapi ini adalah pattern yang sangat umum dan berguna. Ketiga HTTP method (get, put, delete) membutuhkan logic lookup yang sama: mencari Task berdasarkan primary key-nya, atau mengembalikan error 404 yang benar kalau tidak ditemukan. Daripada mengulang block try/except tersebut tiga kali, kita menulisnya satu kali saja dan memanggil self.get_object(pk) dari setiap method.

Http404 dibandingkan membuat Response secara manual

Perhatikan bahwa kita menggunakan raise Http404 daripada me-return Response({"detail": "Not found."}, status=404) secara manual. Ini adalah exception bawaan Django untuk “ini tidak ada”. DRF secara otomatis catch Http404 di manapun di dalam method view dan mengubahnya menjadi JSON response 404 Not Found yang benar untuk kita. Ini berfungsi karena DRF membungkus seluruh logic dispatch view kita di dalam exception handling. Kita tidak perlu menulis Response(...)-nya sendiri. Import seperti ini:

1
from django.http import Http404

Kedua pendekatan (raise Http404 atau me-return Response dengan status 404 secara manual) sama-sama dapat digunakan. Raise Http404 sedikit lebih umum digunakan di dunia DRF begitu kita menggunakan class, karena ini memungkinkan kita men-centralize case “not found” di satu helper method tanpa helper tersebut perlu tahu tentang object Response sama sekali. Tapi kita akan tetap melihat Response(status=404) secara manual digunakan di banyak codebase, keduanya sama-sama benar.


Logic yang self-contained untuk single endpoint

Mari kita melambat sebentar untuk membahas konsep yang disebutkan di outline materi ini: “logic yang self-contained untuk single endpoint”. Ini sebenarnya berkaitan dengan apa yang baru saja kita lihat pada get_object(), mari kita pahami idenya secara eksplisit.

Sebuah class adalah sebuah container. Semua yang dibutuhkan single endpoint (logic lookup-nya, keunikan validasinya, method helper privat kecil apapun) bisa ada di dalam satu class tersebut, dan scope-nya hanya untuk class itu saja. Tidak ada yang leak keluar dan mempengaruhi view lain, begitu juga tidak ada juga leak dari view lain yang masuk.

Bayangkan sebuah toolbox. Setiap subclass APIView adalah toolbox-nya sendiri. TaskDetail punya tool get_object()-nya sendiri di dalam box-nya sendiri. Kalau nanti kita membangun view yang benar-benar berbeda, katakanlah CommentDetail, dia akan mendapatkan toolbox terpisahnya sendiri dengan method get_object()-nya sendiri (mencari Comment, bukan Task). Keduanya tidak saling mengganggu, dan kita tidak perlu khawatir tentang konflik dalam penamaan antara view yang tidak berhubungan, karena setiap method ter-scope ke class-nya sendiri.

Perubahan dalam cara berpikir pada CBV jika dibandingkan dengan FBV terlihat subtle namun sebenarnya penting. Dengan function, kalau kita ingin membagikan sebuah helper antara task_list dan task_detail, kita perlu mendefinisikan helper tersebut di luar kedua function tersebut (mungkin di level module / awal dari file Python) terlebih dahulu, kemudian import ke keduanya. Sementara pada class, terutama begitu kita sampai ke inheritance di materi selanjutnya, cara berbagi logika antar beberapa view adalah dengan membuat parent class yang dipakai bersama. Kita belum melakukan itu di materi ini (kita tidak akan membangun base class bersama hari ini), tapi memahami bahwa class mengelompokkan logic yang berhubungan menjadi dasar mengapa tipe view yang lebih advanced milik DRF (Generic Views, ViewSets) dibangun sebagai class hierarchy.


Permission dan authentication hook

Mulai sekarang kita akan memakai istilah hook. Hook kurang lebih berarti tempat untuk plug in config kita. Hook permission dan authentication adalah salah satu alasan terkuat untuk memilih APIView seiring berkembangnya codebase kita. Mari kita pahami konsepnya dari apa yang sudah kita pelajari.

Rekap singkat: authentication vs permission

Sebelumnya di materi Initial Settings Configuration, kita sudah belajar perbedaannya dan sekarang kita pahami kembali:

  • Authentication menjawab pertanyaan: “Siapa yang membuat request ini?”
  • Permission menjawab pertanyaan: “Apakah user ini diizinkan melakukan ini?”

Keduanya berjalan sebagai sebuah pipeline, dijalankan sebelum method get(), post(), maupun method lainnya, seperti pada skema berikut:

1
2
3
4
5
Request yang masuk
  -> Authentication class berjalan (mengidentifikasi user, kemudian *attach* request.user)
  -> Permission class berjalan (mengecek apakah request.user diizinkan untuk melanjutkan)
  -> Kalau keduanya lolos, method view kita (get/post/put/delete) baru berjalan
  -> Kalau salah satunya gagal, DRF otomatis me-return 401 atau 403, method kita tidak akan pernah berjalan

Ini penting: kalau permission atau authentication gagal, kode get() atau post() kita bahkan tidak akan tereksekusi. DRF menghentikan request tersebut sebelum sampai ke logic kita.

Mengatur authentication dan permission pada view dengan APIView

Kita dapat mengontrol behavior ini dengan dua class attribute:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import SessionAuthentication, BasicAuthentication


class TaskList(APIView):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]

    def get(self, request):
        ...

authentication_classes

authentication_classes adalah kumpulan class yang dicoba oleh DRF, secara berurutan, untuk mencari tahu siapa yang membuat request. Ini adalah opsi yang sama dengan yang kita pelajari di materi Initial Settings sebelumnya (SessionAuthentication, BasicAuthentication, TokenAuthentication, dan lain-lain). Kalau tidak ada satupun yang berhasil, request.user akan menjadi AnonymousUser.

permission_classes

permission_classes adalah kumpulan class yang dicek oleh DRF, secara berurutan, untuk memutuskan apakah request.user (apapun yang dihasilkan oleh authentication) diizinkan untuk melanjutkan. Juga merupakan opsi yang sama dari materi Initial Settings sebelumnya (AllowAny, IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly).

Kenapa CBV lebih nyaman dibandingkan FBV

Dengan FBV, untuk mendapatkan hasil yang sama kita perlu menumpuk decorator:

1
2
3
4
5
6
7
8
9
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import SessionAuthentication, BasicAuthentication

@api_view(["GET", "POST"])
@authentication_classes([SessionAuthentication, BasicAuthentication])
@permission_classes([IsAuthenticated])
def task_list(request):
    ...

Cara diatas memang bisa digunakan, tapi perhatikan bagaimana decorator-decoratornya menumpuk satu sama lain. Sementara dengan class, konfigurasi yang sama hanya menjadi dua baris saja di dalam class body-nya, berdampingan, dan mudah untuk dibaca sekilas:

1
2
3
class TaskList(APIView):
    authentication_classes = [SessionAuthentication, BasicAuthentication]
    permission_classes = [IsAuthenticated]

Global default vs per-view override

Pada materi Initial Settings kita sudah pelajari bahwa apapun yang kita masukkan ke REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] di settings.py adalah fallback untuk semua view. Men-set permission_classes langsung pada sebuah class, seperti yang baru kita lakukan, akan meng-override global default tersebut hanya untuk view spesifik ini saja. Ini persis pattern override yang sudah kita lihat sebelumnya di materi Initial Settings:

1
2
class PublicTaskList(APIView):
    permission_classes = [AllowAny]  # meng-override global default, hanya untuk view ini

Ini memungkinkan kita untuk tetap menjaga global default yang strict (aman secara default) sambil membuka endpoint tertentu secara intentional, satu class pada satu waktu.

Apa yang terjadi kalau kita tidak menyertakan attribute ini sama sekali?

Kalau kita tidak men-set authentication_classes atau permission_classes sama sekali pada class kita, DRF akan fallback ke apapun yang dikonfigurasi secara global di settings.py di bagian REST_FRAMEWORK. Ini adalah behavior fallback global yang sama dengan yang kita pelajari di materi Initial Settings. Untuk latihan materi ini, kita akan men-set permission_classes = [AllowAny] secara eksplisit supaya kita bisa melakukan test secara bebas tanpa perlu login, persis seperti pendekatan fbv_lab di materi Function-Based Views.


Kapan menggunakan APIView dibandingkan shortcut lainnya

DRF sebenarnya menawarkan beberapa layer “shortcut” di atas APIView (Generic Views, Mixins, ViewSets), yang akan kita temui di materi selanjutnya. Mari kita lihat posisi APIView secara apa adanya, agar kita dapat membangun insting yang benar sejak awal.

Berikut adalah framework untuk mengambil keputusan:

SituasiPilihan Terbaik
Endpoint one-off yang cepat dengan logic yang tidak biasa (bukan CRUD sederhana)Function-Based View atau APIView
Behavior CRUD standar (list, create, retrieve, update, delete) yang mengikuti pattern yang umumGeneric Views (materi selanjutnya)
Endpoint yang tidak di-mapping secara clean ke model apapun (health check, search endpoint yang bersinggungan dengan banyak model, custom report)APIView
Kita butuh kontrol yang sangat eksplisit dan mudah dibaca atas setiap baris yang terjadi per HTTP methodAPIView
Kita baru mulai belajar dan ingin melihat dengan tepat bagaimana siklus request/response bekerja, tanpa shortcut yang tersembunyiFunction-Based View atau APIView
Kita mengulang pattern list/create/retrieve/update/delete yang persis sama di banyak modelGeneric Views atau ViewSets (materi selanjutnya), karena APIView berarti kita harus menulis boilerplate tersebut setiap saat

Tradeoff sebenarnya

APIView adalah titik tengah antara FBV dan Generic Views/ViewSets di DRF. APIView memberi kita struktur yang lebih dibandingkan function biasa, tapi tetap mengharuskan kita menulis setiap method body-nya sendiri. Generic Views dan ViewSets (yang akan datang di materi selanjutnya) akan menangani banyak boilerplate tersebut secara otomatis, dengan konsekuensi menjadi kurang eksplisit tentang apa yang terjadi di baliknya.

Untuk saat ini, selagi kita masih membangun mental model tentang siklus request/response, APIView adalah sweet spot-nya. Ini mengajarkan kita struktur yang menjadi dasar dari Generic Views dan tetap memperlihatkan dengan jelas apa yang terjadi di baliknya. Ketika kita akhirnya belajar Generic Views nanti, kita akan langsung mengenali get_object(), permission_classes, dan authentication_classes, karena kita sudah membangun pemahaman itu di sini.

Rule of thumb untuk saat ini: kalau kita tidak yakin apakah sebuah endpoint itu “CRUD sederhana” atau “custom logic”, gunakan APIView. Ini tidak pernah menjadi pilihan yang salah, hanya terkadang menjadi pilihan yang lebih verbose.


Latihan

Kita akan membangun ulang Task API yang serupa dari materi Function-Based Views, tapi kali ini dengan class-based views, di dalam app baru yang sepenuhnya independen.

Latihan ini akan menghasilkan:

  • GET /tasks/: menampilkan list semua task
  • POST /tasks/: membuat task baru
  • GET /tasks/<pk>/: mengambil satu task
  • PUT /tasks/<pk>/: update satu task
  • DELETE /tasks/<pk>/: menghapus satu task

Endpoint yang sama dengan materi Function-Based Views, behavior yang sama, struktur internal yang berbeda.


Buat app dan daftarkan

1
2
3
cd my-drf-project
source .venv/bin/activate
python manage.py startapp cbv_lab

Tambahkan ke INSTALLED_APPS di config/settings.py:

1
2
3
4
INSTALLED_APPS = [
    # ...existing apps...
    "cbv_lab",
]

Buat model di cbv_lab/models.py

Ini sengaja dibuat dengan bentuk yang sama seperti fbv_lab.Task, supaya kita bisa fokus sepenuhnya pada perbedaan layer view-nya, bukan belajar model yang baru.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.db import models


class Task(models.Model):
    class Priority(models.TextChoices):
        LOW = "low", "Low"
        MEDIUM = "medium", "Medium"
        HIGH = "high", "High"

    title = models.CharField(max_length=120)
    notes = models.TextField(blank=True)
    priority = models.CharField(
        max_length=10,
        choices=Priority.choices,
        default=Priority.MEDIUM,
    )
    is_complete = models.BooleanField(default=False)
    due_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

Buat dan apply migrations

1
2
python manage.py makemigrations cbv_lab
python manage.py migrate

Buat serializer di cbv_lab/serializers.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from rest_framework import serializers
from .models import Task


class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = [
            "id",
            "title",
            "notes",
            "priority",
            "is_complete",
            "due_date",
            "created_at",
        ]
        read_only_fields = ["id", "created_at"]

    def validate_title(self, value):
        value = value.strip()
        if len(value) < 3:
            raise serializers.ValidationError("Title must be at least 3 characters.")
        return value

Pattern yang sama dengan yang sudah kita ketahui dari materi Serializer Basics sampai Serializer Methods and Customization, dan identik dengan yang ada di materi Function-Based Views. Tidak ada yang baru di sini.


Buat views di cbv_lab/views.py

Baca setiap baris dengan teliti. Comment-nya menjelaskan setiap keputusan yang diambil.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny

from .models import Task
from .serializers import TaskSerializer


# ============================================================
# LIST VIEW: GET /tasks/ dan POST /tasks/
# ============================================================

class TaskList(APIView):
    """
    GET  /tasks/: return list semua task
    POST /tasks/: membuat task baru
    """

    # AllowAny supaya kita bisa test secara bebas tanpa perlu login.
    # Di project sesungguhnya kita mungkin akan memperketat ini sesuai materi Initial Settings.
    permission_classes = [AllowAny]

    def get(self, request):
        tasks = Task.objects.all()
        # many=True wajib digunakan saat serialize queryset (multiple objects)
        serializer = TaskSerializer(tasks, many=True)
        return Response(serializer.data)

    def post(self, request):
        # request.data berisi JSON body yang sudah di-parse, yang dikirim oleh client
        serializer = TaskSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            # 201 Created adalah status code yang benar ketika resource baru dibuat
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        # 400 Bad Request kalau validasi gagal
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


# ============================================================
# DETAIL VIEW: GET, PUT, DELETE /tasks/<pk>/
# ============================================================

class TaskDetail(APIView):
    """
    GET    /tasks/<pk>/: mengambil satu task
    PUT    /tasks/<pk>/: mengganti task secara keseluruhan
    DELETE /tasks/<pk>/: menghapus task
    """

    permission_classes = [AllowAny]

    def get_object(self, pk):
        """
        Shared lookup logic yang digunakan oleh get(), put(), dan delete().
        Raise Http404 kalau task-nya tidak ada, DRF akan otomatis
        mengubahnya menjadi JSON response 404 yang bersih.
        """
        try:
            return Task.objects.get(pk=pk)
        except Task.DoesNotExist:
            raise Http404

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

    def put(self, request, pk):
        task = self.get_object(pk)
        # Berikan instance yang sudah ada DAN data barunya
        # DRF akan memanggil update() bukan create() karena instance-nya sudah diberikan
        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 = self.get_object(pk)
        task.delete()
        # 204 No Content: berhasil, tapi tidak ada yang perlu di-return
        return Response(status=status.HTTP_204_NO_CONTENT)

Kenapa tidak ada method whitelist kali ini?

Dengan @api_view(["GET", "POST"]) di materi Function-Based Views, kita harus mendaftarkan secara manual method mana saja yang diizinkan. Dengan APIView, kita tidak mendaftarkan apapun. DRF akan melihat method mana saja yang kita definisikan (get, post, dan lain-lain) dan secara otomatis hanya mengizinkan method tersebut. Kalau kita tidak pernah mendefinisikan patch(), request PATCH ke TaskList akan otomatis mendapat 405 Method Not Allowed, tanpa kode tambahan apapun dari kita.


Hubungkan URL-nya

Class-based view membutuhkan satu langkah tambahan yang tidak dibutuhkan oleh function-based view: memanggil .as_view() saat kita mendaftarkannya di urls.py. Kita akan jelaskan alasannya tepat setelah kodenya.

Buat cbv_lab/urls.py:

1
2
3
4
5
6
7
from django.urls import path
from . import views

urlpatterns = [
    path("tasks/", views.TaskList.as_view(), name="cbv-task-list"),
    path("tasks/<int:pk>/", views.TaskDetail.as_view(), name="cbv-task-detail"),
]

Kenapa .as_view()?

Ini adalah perbedaan syntax paling besar antara FBV dan CBV, jadi mari kita pahami dengan jelas.

Sistem URL routing milik Django mengharapkan untuk memanggil sebuah function biasa untuk setiap request yang masuk, yang menerima request sebagai argument pertamanya. Tapi TaskList adalah sebuah class, bukan function. Kita tidak bisa mengarahkan URL secara langsung ke sebuah class.

.as_view() adalah method spesial (di-inherit dari base class View milik Django, yang menjadi dasar APIView) yang mengubah class kita menjadi sesuatu yang berperilaku seperti function dari sudut pandang Django. Setiap kali ada request yang masuk, .as_view() membuat instance baru dari class kita di belakang layar, dan memanggil method yang tepat (get, post, dan lain-lain) pada instance tersebut berdasarkan HTTP method dari request-nya.

Kita tidak perlu memahami internal-nya secara dalam sekarang. Aturan yang perlu diingat sederhana: setiap kali kita mendaftarkan class-based view di urls.py, kita panggil .as_view() padanya. Melupakan hal ini adalah salah satu kesalahan pemula yang paling umum, dan Django akan raise error yang jelas kalau kita lupa (sesuatu seperti “view must be a function” atau “as_view() missing”).


Daftarkan URL app di project

Buka config/urls.py dan include URL app baru kita, langsung di bawah URL materi Function-Based Views:

1
2
3
4
5
6
7
8
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/fbv/", include("fbv_lab.urls")),
    path("api/cbv/", include("cbv_lab.urls")),
]

Endpoint baru kita sekarang menjadi:

  • GET /api/cbv/tasks/
  • POST /api/cbv/tasks/
  • GET /api/cbv/tasks/1/
  • PUT /api/cbv/tasks/1/
  • DELETE /api/cbv/tasks/1/

Tambahkan sample data

1
python manage.py shell
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from datetime import date, timedelta
from cbv_lab.models import Task

Task.objects.all().delete()

today = date.today()

Task.objects.bulk_create([
    Task(title="Set up cbv_lab app", priority="high", due_date=today),
    Task(title="Write views.py with APIView", priority="high", due_date=today + timedelta(days=1)),
    Task(title="Test GET list endpoint", priority="medium", due_date=today + timedelta(days=2)),
    Task(title="Test POST endpoint", priority="medium", due_date=today + timedelta(days=3)),
    Task(title="Test 404 handling", priority="low", due_date=today + timedelta(days=4)),
])

print(Task.objects.count())  # 5

Test endpoint di browser atau Postman

Keluar dari shell setelah selesai (exit() atau Ctrl-D), lalu jalankan server di bawah ini.

1
python manage.py runserver

Kunjungi http://127.0.0.1:8000/api/cbv/tasks/ di browser. Kita seharusnya melihat browsable API interface yang sama seperti yang kita lihat di materi Function-Based Views, menampilkan list lima task yang sudah kita seed.

Coba http://127.0.0.1:8000/api/cbv/tasks/1/ untuk melihat satu task.

Coba yang tidak ada, http://127.0.0.1:8000/api/cbv/tasks/9999/, dan pastikan kita mendapat response 404 yang clean, bukan server error.


Test full request/response cycle di shell

Pendekatan simulasi yang sama dengan materi Function-Based Views, hanya saja kali ini kita memanggil method class baru kita secara langsung untuk melihatnya bekerja secara terisolasi.

1
python manage.py shell
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from cbv_lab.models import Task
from cbv_lab.serializers import TaskSerializer
from cbv_lab.views import TaskList, TaskDetail
from rest_framework import status

# --- Simulasikan GET list (hanya logic serializer, sama seperti sebelumnya) ---
tasks = Task.objects.all()
serializer = TaskSerializer(tasks, many=True)
print("Status akan menjadi:", status.HTTP_200_OK)
print("Items:", len(serializer.data))

# --- Simulasikan POST (valid) ---
valid_payload = {
    "title": "Review Steve's iOS pull request",
    "notes": "Check the audio player state handling",
    "priority": "high",
    "is_complete": False,
    "due_date": "2027-08-01",
}
ser = TaskSerializer(data=valid_payload)
print(ser.is_valid())       # True
new_task = ser.save()
print(f"id: {new_task.id}, title: {new_task.title}")

# --- Test get_object() secara langsung pada instance TaskDetail ---
detail_view = TaskDetail()
found_task = detail_view.get_object(new_task.id)
print(found_task.title)     # "Review Steve's iOS pull request"

# --- Test get_object() yang raise Http404 untuk pk yang tidak ada ---
from django.http import Http404
try:
    detail_view.get_object(999999)
except Http404:
    print("Akan return 404, sesuai ekspektasi")

# --- Simulasikan DELETE ---
task_to_delete = Task.objects.last()
task_id = task_to_delete.id
task_to_delete.delete()
print(f"Task {task_id} dihapus. Akan return 204:", status.HTTP_204_NO_CONTENT)
print("Task yang tersisa:", Task.objects.count())

Alur lengkap dari request hingga response

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Client
  |
  | HTTP Request (method + URL + body)
  v
config/urls.py               <-- "app mana yang menangani URL ini?"
  |
cbv_lab/urls.py               <-- "class view mana yang menangani path spesifik ini?"
  |
TaskList.as_view()            <-- mengubah class kita menjadi sesuatu yang bisa dipanggil oleh router Django
  |
APIView.dispatch()            <-- (inherited, tidak terlihat oleh kita) menjalankan authentication, lalu permission
  |                                 kalau salah satunya gagal, berhenti di sini dan return 401/403
  |
get() / post() / dll. milik kita <-- membaca request.method (secara implisit, lewat nama method), request.data,
  |                                 request.query_params
  |
get_object() (kalau kita menulisnya) <-- shared lookup helper, ter-scope ke class ini
  |
serializer                    <-- memvalidasi input (POST/PUT) atau memformat output (GET)
  |
model / ORM                   <-- membaca dari atau menulis ke database
  |
Response(data, status)        <-- DRF membangun JSON response-nya
  |
  | HTTP Response (status code + JSON body)
  v
Client

Langkah baru yang penting dibandingkan diagram di materi Function-Based Views adalah APIView.dispatch(). Ini adalah mesin yang kita dapatkan secara gratis lewat inheritance, dan di sinilah persis tempat authentication dan permission check terjadi, sebelum method kita bahkan mulai berjalan.


Tabel perbandingan: Function-Based View vs Class-Based View (APIView)

FeatureFunction-Based View (@api_view)Class-Based View (APIView)
Bagaimana method ditanganiSatu function, if request.method == "..."Satu method per HTTP verb (get, post, put, delete)
Method whitelistList eksplisit yang diberikan ke decoratorImplisit, berdasarkan method mana yang kita definisikan
Registrasi URLpath("tasks/", views.task_list)path("tasks/", views.TaskList.as_view())
Setup logic yang dipakai bersama (misalnya object lookup)Diulang di setiap if block, atau ditarik keluar menjadi function di level moduleSebuah method pada class-nya (misalnya get_object()), dipanggil dari beberapa method
Permission dan authenticationDecorator yang ditumpuk di atas (@permission_classes, @authentication_classes)Class attribute (permission_classes, authentication_classes)
Cocok untukEndpoint yang kecil, sederhana, one-offEndpoint dengan shared logic, atau sebagai dasar untuk Generic Views nanti
Keterbacaan saat berkembangSemakin sulit dibaca seiring bertambahnya method dan branchTetap terorganisir karena setiap method secara fisik terpisah

Kesalahan yang sering terjadi

  • Lupa .as_view() di urls.py. Django butuh sesuatu yang callable, bukan class polos. path("tasks/", views.TaskList) akan raise error.

  • Menamai method dengan sesuatu selain HTTP verb dalam lowercase. Kalau kita menulis def GET(self, request): (uppercase) atau def list(self, request):, DRF tidak akan mengenalinya sebagai handler untuk request GET. Harus persis get, post, put, patch, atau delete, semuanya lowercase.

  • Lupa self sebagai parameter pertama. Setiap method di dalam sebuah class membutuhkan self. Menulis def get(request): tanpa self akan raise TypeError saat Django mencoba memanggilnya.

  • Tidak men-set permission_classes dan terkejut ketika sebuah view tiba-tiba membutuhkan login. Kalau kita tidak men-set-nya secara eksplisit, view tersebut akan fallback ke apapun yang dikonfigurasi secara global di settings.py di bawah REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"], yang mungkin bukan AllowAny.

  • Menulis shared lookup logic (seperti get_object()) tapi lupa untuk benar-benar memanggilnya dari setiap method yang membutuhkannya, sehingga menyebabkan behavior yang tidak konsisten antara get(), put(), dan delete().

  • Tertukar antara Http404 (dari django.http) dengan exception class milik DRF sendiri. raise Http404 berfungsi karena DRF secara khusus men-catch-nya, tapi mudah untuk tidak sengaja mengimport hal yang salah kalau kita tidak memperhatikan.

  • Menganggap APIView otomatis menangani boilerplate CRUD seperti yang akan dilakukan Generic Views nanti. APIView tetap mengharuskan kita menulis setiap baris logic sendiri. Ini wajar dan benar pada tahap ini.


Ringkasan

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
APIView                          -> base class untuk class-based views, diimport dari rest_framework.views
def get(self, request):          -> menangani request GET, nama method langsung mapping ke HTTP verb
def post(self, request):         -> menangani request POST
def put(self, request, pk):      -> menangani request PUT
def delete(self, request, pk):   -> menangani request DELETE
.as_view()                       -> wajib di urls.py untuk mengubah class menjadi sesuatu yang bisa di-routing Django
get_object()                     -> pattern custom helper yang umum untuk shared lookup logic antar method
raise Http404                    -> DRF otomatis mengubah ini menjadi JSON response 404 yang bersih
permission_classes               -> class attribute, list permission class, meng-override global default
authentication_classes           -> class attribute, list authentication class, meng-override global default
HTTP method yang tidak diimplementasikan -> otomatis return 405 Method Not Allowed, tanpa kode tambahan

Foto cover oleh fallout766 dari Unsplash

Dibawah Lisensi CC BY-NC-SA 4.0
Dibangun dengan Hugo
Tema Stack dirancang oleh Jimmy