Membangun REST API dengan Django REST Framework (DRF): Serializers, ViewSets, dan Routers, autentikasi & permission, pagination, serta praktik lengkap REST API CRUD untuk post blog dengan autentikasi JWT memakai SimpleJWT.

Sampai episode 10, semua output adalah HTML untuk browser. Tapi dunia 2026 butuh lebih dari itu: aplikasi mobile, SPA, dan integrasi pihak ketiga berbicara lewat API. Di episode ini kita membangun REST API dengan Django REST Framework (DRF) — framework de facto untuk API di ekosistem Django.
Mengapa topik ini penting? Karena API adalah antarmuka publik kedua dari aplikasi kalian (selain halaman web), dan antarmuka ini paling sering di-scrape, di-spam, dan diserang. DRF mengotomatiskan serializer, router, dan dokumentasi, tetapi autentikasi dan permission tetap keputusan kalian — dan di situlah keamanan API ditentukan.
pip install djangorestframework djangorestframework-simplejwtINSTALLED_APPS = [
...
"rest_framework",
"rest_framework_simplejwt",
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
),
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}DEFAULT_PERMISSION_CLASSES default IsAuthenticated memaksa semua endpoint login dulu — kita longgarkan per-endpoint untuk data publik (post list). Pagination diaktifkan global dengan PAGE_SIZE.
Serializer adalah jembatan model ↔ JSON: ia memutuskan field mana yang diekspos dan bagaimana validasinya.
from rest_framework import serializers
from .models import Comment, Post
class CommentSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField(read_only=True)
class Meta:
model = Comment
fields = ["id", "author", "body", "created_at"]
read_only_fields = ["author", "post"]
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField(read_only=True)
category = serializers.SlugRelatedField(slug_field="slug", read_only=True)
comment_count = serializers.IntegerField(read_only=True)
class Meta:
model = Post
fields = ["id", "title", "slug", "body", "status", "category",
"author", "comment_count", "created_at"]ModelSerializer menurunkan field dari model. StringRelatedField menampilkan __str__ relasi. comment_count adalah field virtual yang akan kita isi dari annotate — bukan field model. Serializer inilah yang memisahkan "data publik" dari "data internal" (misal status tidak boleh di-set client secara bebas).
ViewSet menggabungkan action list/detail/create/update/delete dalam satu class. Router lalu men-generate URL-nya secara otomatis:
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAdminUser
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.filter(status="published")
serializer_class = PostSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
lookup_field = "slug"
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_staff:
qs = Post.objects.all()
return qs.annotate(comment_count=Count("comments")).select_related("author", "category")
@action(detail=False, methods=["post"], permission_classes=[IsAdminUser])
def publish_all(self, request):
updated = Post.objects.filter(status="draft").update(status="published")
return Response({"updated": updated})IsAuthenticatedOrReadOnly mengizinkan GET publik, menuntut auth untuk write. get_queryset di-override agar staff bisa melihat draft dan setiap post membawa comment_count (anti-N+1 dari episode 6). Custom action publish_all menambahkan endpoint /api/posts/publish_all/ yang hanya bisa dipanggil admin.
URL:
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from blog import views
router = DefaultRouter()
router.register("posts", views.PostViewSet)
urlpatterns = [
path("api/", include(router.urls)),
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]DefaultRouter men-generate route list (GET/POST /api/posts/), detail (GET/PUT/PATCH/DELETE /api/posts/<slug>/), dan halaman dokumentasi browsable API.
JWT (JSON Web Token) membawa klaim (user id, expiry) yang ditandatangani, sehingga server tidak perlu menyimpan state session. SimpleJWT menyediakan endpoint token:
curl -X POST http://127.0.0.1:8000/api/token/ \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "rahasia"}'Respons berupa access dan refresh token:
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Lalu kirim access token di header Authorization: Bearer <token>:
curl http://127.0.0.1:8000/api/posts/ \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Konfigurasi dasar SimpleJWT di settings:
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"AUTH_HEADER_TYPES": ("Bearer",),
"ALGORITHM": "HS256",
}Warning
Jangan menaruh data sensitif di payload JWT — payload hanya di-encode Base64, bukan dienkripsi; siapa pun bisa membacanya. ACCESS_TOKEN_LIFETIME pendek (15 menit) meminimalkan dampak token bocor; refresh dipakai untuk mendapat access baru. Seluk-beluk refresh, revoke, dan rotasi kita bahas di episode 20.
Pagination sudah aktif global: respons berbentuk:
{
"count": 120,
"next": "http://127.0.0.1:8000/api/posts/?page=2",
"previous": null,
"results": [ ... ]
}Untuk filtering, pasang django-filter agar field bisa difilter via query param:
pip install django-filterREST_FRAMEWORK = {
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
],
}from django_filters.rest_framework import DjangoFilterBackend
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.filter(status="published")
serializer_class = PostSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ["category__slug", "author__username"]Sekarang GET /api/posts/?category__slug=django&author__username=admin bekerja langsung. Kombinasi filter + pagination adalah standar API production yang harus kalian biasakan.
Inti yang harus dibawa pulang:
ModelSerializer (transformasi data) + ViewSet (logika CRUD) + Router (URL otomatis).permission_classes mengatur akses: IsAuthenticatedOrReadOnly untuk API publik+private.access (pendek, 15 menit) + refresh (panjang) dikirim via header Authorization: Bearer.PageNumberPagination; filtering via django-filter.comment_count di get_queryset untuk performa (N+1 episode 6).Di episode 12 selanjutnya kita menangani beban baca: Cache & Sessions — framework cache Django (LocMem, Redis, Memcached), caching per-view dan per-template, serta memindahkan session ke Redis agar login cepat dan scalable. Sampai jumpa di episode 12!