Django #
Django adalah web framework full-stack untuk Python yang mengikuti filosofi “batteries included” — ORM, autentikasi, admin interface, form handling, template engine, dan keamanan semuanya sudah tersedia tanpa perlu library tambahan. Django dirancang untuk membangun aplikasi web yang kompleks dengan cepat dan aman, mengikuti pola arsitektur MTV (Model-Template-View). Cocok untuk aplikasi dengan banyak fitur, tim yang besar, dan kebutuhan keamanan tinggi seperti platform e-commerce, CMS, dan aplikasi enterprise.
Instalasi dan Setup Proyek #
pip install django
# Buat proyek baru
django-admin startproject myproject
cd myproject
# Buat aplikasi dalam proyek
python manage.py startapp produk
# Jalankan server development
python manage.py runserver
# Server berjalan di http://127.0.0.1:8000/
Struktur direktori yang dihasilkan:
myproject/
├── manage.py # CLI tool untuk manajemen proyek
├── myproject/
│ ├── settings.py # konfigurasi global
│ ├── urls.py # URL routing utama
│ ├── wsgi.py # entry point untuk WSGI server
│ └── asgi.py # entry point untuk ASGI server
└── produk/ # aplikasi "produk"
├── models.py # definisi model/tabel database
├── views.py # logika request/response
├── urls.py # URL routing aplikasi
├── admin.py # konfigurasi Django admin
├── apps.py # konfigurasi aplikasi
└── migrations/ # file migrasi database
Konfigurasi #
# myproject/settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-secret-key-ganti-di-produksi")
DEBUG = os.environ.get("DEBUG", "True") == "True"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"produk", # daftarkan aplikasi kita
]
# Database
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "myapp"),
"USER": os.environ.get("DB_USER", "postgres"),
"PASSWORD": os.environ.get("DB_PASSWORD", ""),
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
LANGUAGE_CODE = "id"
TIME_ZONE = "Asia/Jakarta"
USE_TZ = True
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
Jangan gunakanSECRET_KEYdefault di produksi. Buat secret key baru denganpython -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"dan simpan di environment variable atau file.env. Pastikan jugaDEBUG=Falsedi produksi.
Model #
Model Django mendefinisikan skema database sebagai kelas Python. Setelah mendefinisikan model, jalankan makemigrations dan migrate untuk membuat tabel.
# produk/models.py
from django.db import models
from django.utils.text import slugify
class Kategori(models.Model):
nama = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=120, unique=True, blank=True)
class Meta:
db_table = "kategori"
verbose_name_plural = "Kategori"
ordering = ["nama"]
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.nama)
super().save(*args, **kwargs)
def __str__(self):
return self.nama
class Produk(models.Model):
nama = models.CharField(max_length=200)
slug = models.SlugField(max_length=220, unique=True, blank=True)
deskripsi = models.TextField(blank=True)
harga = models.DecimalField(max_digits=15, decimal_places=2)
stok = models.PositiveIntegerField(default=0)
aktif = models.BooleanField(default=True)
gambar = models.ImageField(upload_to="produk/", null=True, blank=True)
kategori = models.ForeignKey(
Kategori,
on_delete=models.SET_NULL,
null=True, blank=True,
related_name="produk"
)
dibuat_pada = models.DateTimeField(auto_now_add=True)
diubah_pada = models.DateTimeField(auto_now=True)
class Meta:
db_table = "produk"
ordering = ["-dibuat_pada"]
indexes = [
models.Index(fields=["aktif", "dibuat_pada"]),
models.Index(fields=["kategori", "aktif"]),
]
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.nama)
super().save(*args, **kwargs)
def __str__(self):
return self.nama
# Buat dan jalankan migrasi
python manage.py makemigrations
python manage.py migrate
Django Admin #
Admin interface Django memungkinkan pengelolaan data langsung dari browser tanpa menulis kode CRUD.
# produk/admin.py
from django.contrib import admin
from .models import Kategori, Produk
@admin.register(Kategori)
class KategoriAdmin(admin.ModelAdmin):
list_display = ["nama", "slug"]
search_fields = ["nama"]
prepopulated_fields = {"slug": ("nama",)}
@admin.register(Produk)
class ProdukAdmin(admin.ModelAdmin):
list_display = ["nama", "kategori", "harga", "stok", "aktif", "dibuat_pada"]
list_filter = ["aktif", "kategori"]
search_fields = ["nama", "deskripsi"]
list_editable = ["aktif", "stok"]
prepopulated_fields = {"slug": ("nama",)}
readonly_fields = ["dibuat_pada", "diubah_pada"]
date_hierarchy = "dibuat_pada"
# Buat superuser untuk akses admin
python manage.py createsuperuser
# Akses di: http://127.0.0.1:8000/admin/
Views #
Django mendukung dua pendekatan: function-based views (FBV) yang sederhana, dan class-based views (CBV) yang lebih terstruktur untuk CRUD.
Function-Based Views #
# produk/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.http import require_http_methods
from django.core.paginator import Paginator
from .models import Produk, Kategori
def daftar_produk(request):
"""Tampilkan daftar produk dengan filter dan pagination."""
qs = Produk.objects.filter(aktif=True).select_related("kategori")
# Filter berdasarkan query parameter
kategori_slug = request.GET.get("kategori")
if kategori_slug:
qs = qs.filter(kategori__slug=kategori_slug)
q = request.GET.get("q")
if q:
qs = qs.filter(nama__icontains=q)
# Pagination
paginator = Paginator(qs, per_page=12)
halaman = request.GET.get("halaman", 1)
produk_page = paginator.get_page(halaman)
return render(request, "produk/daftar.html", {
"produk_list": produk_page,
"kategori_list": Kategori.objects.all(),
"q": q or "",
})
def detail_produk(request, slug):
produk = get_object_or_404(Produk, slug=slug, aktif=True)
return render(request, "produk/detail.html", {"produk": produk})
@require_http_methods(["POST"])
def tambah_produk_api(request):
"""Endpoint API sederhana untuk membuat produk."""
import json
try:
data = json.loads(request.body)
produk = Produk.objects.create(
nama=data["nama"],
harga=data["harga"],
stok=data.get("stok", 0),
)
return JsonResponse({"id": produk.id, "nama": produk.nama}, status=201)
except (KeyError, json.JSONDecodeError) as e:
return HttpResponseBadRequest(f"Data tidak valid: {e}")
Class-Based Views #
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
class ProdukListView(ListView):
model = Produk
template_name = "produk/daftar.html"
context_object_name = "produk_list"
paginate_by = 12
def get_queryset(self):
qs = super().get_queryset().filter(aktif=True).select_related("kategori")
q = self.request.GET.get("q")
if q:
qs = qs.filter(nama__icontains=q)
return qs
class ProdukDetailView(DetailView):
model = Produk
template_name = "produk/detail.html"
context_object_name = "produk"
slug_field = "slug"
class ProdukCreateView(LoginRequiredMixin, CreateView):
model = Produk
fields = ["nama", "deskripsi", "harga", "stok", "kategori", "gambar"]
template_name = "produk/form.html"
success_url = reverse_lazy("produk:daftar")
class ProdukUpdateView(LoginRequiredMixin, UpdateView):
model = Produk
fields = ["nama", "deskripsi", "harga", "stok", "aktif", "kategori"]
template_name = "produk/form.html"
success_url = reverse_lazy("produk:daftar")
class ProdukDeleteView(LoginRequiredMixin, DeleteView):
model = Produk
template_name = "produk/konfirmasi_hapus.html"
success_url = reverse_lazy("produk:daftar")
URL Routing #
# produk/urls.py
from django.urls import path
from . import views
app_name = "produk" # namespace untuk reverse URL
urlpatterns = [
path("", views.ProdukListView.as_view(), name="daftar"),
path("<slug:slug>/", views.ProdukDetailView.as_view(), name="detail"),
path("tambah/", views.ProdukCreateView.as_view(), name="tambah"),
path("<slug:slug>/edit/", views.ProdukUpdateView.as_view(), name="edit"),
path("<slug:slug>/hapus/", views.ProdukDeleteView.as_view(), name="hapus"),
path("api/produk/", views.tambah_produk_api, name="api-tambah"),
]
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("produk/", include("produk.urls", namespace="produk")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Template #
<!-- produk/templates/produk/daftar.html -->
{% extends "base.html" %}
{% block title %}Daftar Produk{% endblock %}
{% block content %}
<div class="container">
<!-- Form pencarian -->
<form method="get">
<input type="text" name="q" value="{{ q }}" placeholder="Cari produk...">
<button type="submit">Cari</button>
</form>
<!-- Daftar produk -->
<div class="produk-grid">
{% for produk in produk_list %}
<div class="produk-card">
{% if produk.gambar %}
<img src="{{ produk.gambar.url }}" alt="{{ produk.nama }}">
{% endif %}
<h3><a href="{% url 'produk:detail' produk.slug %}">{{ produk.nama }}</a></h3>
<p>{{ produk.kategori.nama }}</p>
<p>Rp{{ produk.harga|floatformat:0 }}</p>
<p>Stok: {{ produk.stok }}</p>
</div>
{% empty %}
<p>Tidak ada produk yang ditemukan.</p>
{% endfor %}
</div>
<!-- Pagination -->
{% if produk_list.has_other_pages %}
<nav>
{% if produk_list.has_previous %}
<a href="?halaman={{ produk_list.previous_page_number }}">← Sebelumnya</a>
{% endif %}
<span>Halaman {{ produk_list.number }} dari {{ produk_list.paginator.num_pages }}</span>
{% if produk_list.has_next %}
<a href="?halaman={{ produk_list.next_page_number }}">Berikutnya →</a>
{% endif %}
</nav>
{% endif %}
</div>
{% endblock %}
REST API dengan Django REST Framework #
Untuk membangun JSON API, gunakan Django REST Framework (DRF):
pip install djangorestframework
# settings.py -- tambahkan ke INSTALLED_APPS
INSTALLED_APPS = [
...
"rest_framework",
]
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
}
# produk/serializers.py
from rest_framework import serializers
from .models import Produk, Kategori
class KategoriSerializer(serializers.ModelSerializer):
class Meta:
model = Kategori
fields = ["id", "nama", "slug"]
class ProdukSerializer(serializers.ModelSerializer):
kategori = KategoriSerializer(read_only=True)
kategori_id = serializers.PrimaryKeyRelatedField(
queryset=Kategori.objects.all(),
source="kategori",
write_only=True,
required=False,
allow_null=True
)
class Meta:
model = Produk
fields = [
"id", "nama", "slug", "deskripsi",
"harga", "stok", "aktif",
"kategori", "kategori_id",
"dibuat_pada", "diubah_pada"
]
read_only_fields = ["id", "slug", "dibuat_pada", "diubah_pada"]
def validate_harga(self, value):
if value <= 0:
raise serializers.ValidationError("Harga harus lebih dari 0.")
return value
# produk/api_views.py
from rest_framework import viewsets, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Produk
from .serializers import ProdukSerializer
class ProdukViewSet(viewsets.ModelViewSet):
queryset = Produk.objects.filter(aktif=True).select_related("kategori")
serializer_class = ProdukSerializer
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ["aktif", "kategori"]
search_fields = ["nama", "deskripsi"]
ordering_fields = ["harga", "dibuat_pada", "stok"]
ordering = ["-dibuat_pada"]
@action(detail=False, methods=["get"])
def stok_habis(self, request):
"""Custom action: ambil produk dengan stok = 0."""
qs = self.get_queryset().filter(stok=0)
serializer = self.get_serializer(qs, many=True)
return Response(serializer.data)
# produk/api_urls.py
from rest_framework.routers import DefaultRouter
from .api_views import ProdukViewSet
router = DefaultRouter()
router.register("produk", ProdukViewSet, basename="produk")
urlpatterns = router.urls
# Endpoint yang dihasilkan otomatis:
# GET /api/produk/ -- list
# POST /api/produk/ -- create
# GET /api/produk/{id}/ -- retrieve
# PUT /api/produk/{id}/ -- update
# PATCH /api/produk/{id}/ -- partial update
# DELETE /api/produk/{id}/ -- destroy
# GET /api/produk/stok_habis/ -- custom action
Middleware Kustom #
# myproject/middleware.py
import time
import logging
logger = logging.getLogger(__name__)
class RequestTimingMiddleware:
"""Log waktu eksekusi setiap request."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
mulai = time.time()
response = self.get_response(request)
durasi = (time.time() - mulai) * 1000 # milidetik
logger.info(
f"{request.method} {request.path} "
f"→ {response.status_code} ({durasi:.1f}ms)"
)
return response
# settings.py -- tambahkan ke MIDDLEWARE
MIDDLEWARE = [
...
"myproject.middleware.RequestTimingMiddleware",
]
Manajemen dan Deployment #
# Kumpulkan static files untuk production
python manage.py collectstatic
# Cek keamanan konfigurasi sebelum deploy
python manage.py check --deploy
# Buat custom management command
# myapp/management/commands/bersihkan_data.py
# myapp/management/commands/bersihkan_data.py
from django.core.management.base import BaseCommand
from produk.models import Produk
class Command(BaseCommand):
help = "Hapus produk yang tidak aktif lebih dari 30 hari"
def add_arguments(self, parser):
parser.add_argument("--dry-run", action="store_true", help="Preview saja tanpa hapus")
def handle(self, *args, **options):
from django.utils import timezone
from datetime import timedelta
batas = timezone.now() - timedelta(days=30)
qs = Produk.objects.filter(aktif=False, diubah_pada__lt=batas)
self.stdout.write(f"Ditemukan {qs.count()} produk untuk dihapus.")
if not options["dry_run"]:
qs.delete()
self.stdout.write(self.style.SUCCESS("Selesai."))
else:
self.stdout.write("Dry run -- tidak ada yang dihapus.")
python manage.py bersihkan_data --dry-run
python manage.py bersihkan_data
Ringkasan #
- Batteries included — Django menyertakan ORM, autentikasi, admin, form, template engine, dan keamanan CSRF/XSS secara bawaan; tidak perlu rakitan manual seperti Flask.
SECRET_KEYdanDEBUG— selalu baca dari environment variable; jangan pernah commit nilai production ke repository.select_related()danprefetch_related()— selalu gunakan saat mengakses relasi dalam loop untuk menghindari N+1 query.get_object_or_404()— gunakan alih-alihModel.objects.get()di views agar otomatis return 404 jika tidak ditemukan.- Django Admin — manfaatkan untuk manajemen data internal; kustomisasi dengan
list_display,list_filter, dansearch_fieldsagar lebih produktif.- CBV untuk CRUD standar — gunakan
ListView,DetailView,CreateView,UpdateView,DeleteViewuntuk mengurangi boilerplate; gunakan FBV untuk logika yang lebih kompleks atau custom.- Namespace URL — selalu gunakan
app_namediurls.pydan{% url 'app:name' %}di template agar URL bisa di-refactor tanpa mengubah template.- Django REST Framework — gunakan
ViewSet+Routeruntuk membangun REST API yang konsisten; manfaatkanserializersuntuk validasi input sekaligus serialisasi output.python manage.py check --deploy— jalankan sebelum setiap deployment untuk mengecek konfigurasi keamanan secara otomatis.- Middleware untuk cross-cutting concerns — logging, timing, autentikasi kustom, dan header keamanan paling bersih diimplementasikan sebagai middleware.