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 default SECRET_KEY in production. Generate a new secret key with python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" and store it in an environment variable or .env file. Also make sure DEBUG=False in 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_KEY and DEBUG — always read them from environment variables; never commit production values to the repository.
  • select_related() and prefetch_related() — always use them when accessing relationships in a loop to avoid N+1 queries.
  • get_object_or_404() — use it instead of Model.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, and search_fields to be more productive.
  • CBVs for standard CRUD — use ListView, DetailView, CreateView, UpdateView, DeleteView to reduce boilerplate; use FBVs for more complex or custom logic.
  • URL namespaces — always use app_name in urls.py and {% url 'app:name' %} in templates so URLs can be refactored without changing templates.
  • Django REST Framework — use ViewSet + Router to build consistent REST APIs; leverage serializers for 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.

← Previous: Memcached   Next: FastAPI →

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