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 gunakan SECRET_KEY default di produksi. Buat secret key baru dengan python -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 juga DEBUG=False di 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_KEY dan DEBUG — selalu baca dari environment variable; jangan pernah commit nilai production ke repository.
  • select_related() dan prefetch_related() — selalu gunakan saat mengakses relasi dalam loop untuk menghindari N+1 query.
  • get_object_or_404() — gunakan alih-alih Model.objects.get() di views agar otomatis return 404 jika tidak ditemukan.
  • Django Admin — manfaatkan untuk manajemen data internal; kustomisasi dengan list_display, list_filter, dan search_fields agar lebih produktif.
  • CBV untuk CRUD standar — gunakan ListView, DetailView, CreateView, UpdateView, DeleteView untuk mengurangi boilerplate; gunakan FBV untuk logika yang lebih kompleks atau custom.
  • Namespace URL — selalu gunakan app_name di urls.py dan {% url 'app:name' %} di template agar URL bisa di-refactor tanpa mengubah template.
  • Django REST Framework — gunakan ViewSet + Router untuk membangun REST API yang konsisten; manfaatkan serializers untuk 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.

← Sebelumnya: Memcached   Berikutnya: FastAPI →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact