Django #
Django is a full-stack web framework for Python following the “batteries included” philosophy — ORM, authentication, admin interface, form handling, template engine, and security are all available without extra libraries. Django is designed to build complex web applications quickly and securely, following the MTV (Model-Template-View) architecture pattern. It suits applications with many features, large teams, and high security needs such as e-commerce platforms, CMS, and enterprise applications.
Installation and Project Setup #
pip install django
# Create a new project
django-admin startproject myproject
cd myproject
# Create an app within the project
python manage.py startapp produk
# Run the development server
python manage.py runserver
# Server running at http://127.0.0.1:8000/
The resulting directory structure:
myproject/
├── manage.py # CLI tool for project management
├── myproject/
│ ├── settings.py # global configuration
│ ├── urls.py # main URL routing
│ ├── wsgi.py # entry point for WSGI servers
│ └── asgi.py # entry point for ASGI servers
└── produk/ # the "produk" app
├── models.py # model/database table definitions
├── views.py # request/response logic
├── urls.py # app URL routing
├── admin.py # Django admin configuration
├── apps.py # app configuration
└── migrations/ # database migration files
Configuration #
# 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-change-in-production")
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", # register our app
]
# 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"
Don’t use the defaultSECRET_KEYin production. Generate a new secret key withpython -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"and store it in an environment variable or.envfile. Also make sureDEBUG=Falsein production.
Models #
Django models define the database schema as Python classes. After defining models, run makemigrations and migrate to create the tables.
# 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
# Create and run migrations
python manage.py makemigrations
python manage.py migrate
Django Admin #
Django’s admin interface allows managing data directly from the browser without writing CRUD code.
# 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"
# Create a superuser for admin access
python manage.py createsuperuser
# Access at: http://127.0.0.1:8000/admin/
Views #
Django supports two approaches: simple function-based views (FBVs), and class-based views (CBVs) that are more structured for 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):
"""Show the product list with filtering and pagination."""
qs = Produk.objects.filter(aktif=True).select_related("kategori")
# Filter by 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):
"""A simple API endpoint to create a product."""
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"Invalid data: {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 for URL reversing
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)
Templates #
<!-- produk/templates/produk/daftar.html -->
{% extends "base.html" %}
{% block title %}Product List{% endblock %}
{% block content %}
<div class="container">
<!-- Search form -->
<form method="get">
<input type="text" name="q" value="{{ q }}" placeholder="Search products...">
<button type="submit">Search</button>
</form>
<!-- Product list -->
<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>Stock: {{ produk.stok }}</p>
</div>
{% empty %}
<p>No products found.</p>
{% endfor %}
</div>
<!-- Pagination -->
{% if produk_list.has_other_pages %}
<nav>
{% if produk_list.has_previous %}
<a href="?halaman={{ produk_list.previous_page_number }}">← Previous</a>
{% endif %}
<span>Page {{ produk_list.number }} of {{ produk_list.paginator.num_pages }}</span>
{% if produk_list.has_next %}
<a href="?halaman={{ produk_list.next_page_number }}">Next →</a>
{% endif %}
</nav>
{% endif %}
</div>
{% endblock %}
REST APIs with Django REST Framework #
To build JSON APIs, use Django REST Framework (DRF):
pip install djangorestframework
# settings.py -- add to 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("Price must be greater than 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: fetch products with stock = 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
# Automatically generated endpoints:
# 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
Custom Middleware #
# myproject/middleware.py
import time
import logging
logger = logging.getLogger(__name__)
class RequestTimingMiddleware:
"""Log the execution time of every 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 # milliseconds
logger.info(
f"{request.method} {request.path} "
f"→ {response.status_code} ({durasi:.1f}ms)"
)
return response
# settings.py -- add to MIDDLEWARE
MIDDLEWARE = [
...
"myproject.middleware.RequestTimingMiddleware",
]
Management and Deployment #
# Collect static files for production
python manage.py collectstatic
# Check the security configuration before deploying
python manage.py check --deploy
# Create a 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 = "Delete products inactive for more than 30 days"
def add_arguments(self, parser):
parser.add_argument("--dry-run", action="store_true", help="Preview only without deleting")
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"Found {qs.count()} products to delete.")
if not options["dry_run"]:
qs.delete()
self.stdout.write(self.style.SUCCESS("Done."))
else:
self.stdout.write("Dry run -- nothing deleted.")
python manage.py bersihkan_data --dry-run
python manage.py bersihkan_data
Summary #
- Batteries included — Django ships with an ORM, authentication, admin, forms, template engine, and CSRF/XSS security built in; no manual assembly like Flask.
SECRET_KEYandDEBUG— always read them from environment variables; never commit production values to the repository.select_related()andprefetch_related()— always use them when accessing relationships in a loop to avoid N+1 queries.get_object_or_404()— use it instead ofModel.objects.get()in views so a 404 is returned automatically when not found.- Django Admin — leverage it for internal data management; customize with
list_display,list_filter, andsearch_fieldsto be more productive.- CBVs for standard CRUD — use
ListView,DetailView,CreateView,UpdateView,DeleteViewto reduce boilerplate; use FBVs for more complex or custom logic.- URL namespaces — always use
app_nameinurls.pyand{% url 'app:name' %}in templates so URLs can be refactored without changing templates.- Django REST Framework — use
ViewSet+Routerto build consistent REST APIs; leverageserializersfor input validation and output serialization at once.python manage.py check --deploy— run it before every deployment to automatically check the security configuration.- Middleware for cross-cutting concerns — logging, timing, custom authentication, and security headers are cleanest implemented as middleware.