Website Backend Development with Python (Django)
Django is "batteries included" in the literal sense. ORM, migrations, admin panel, authentication, forms, caching, i18n — all included out of the box, without library selection and component integration. This makes Django the fastest way to launch a full-fledged backend when you don't need exotic features.
Typical use cases: corporate websites, CMS, portals, APIs for SPA and mobile applications, systems with rich business logic and role-based access.
Project Structure
A Django project consists of a project and apps. Good practice is to keep apps small and functionally isolated:
myproject/
manage.py
config/
settings/
base.py
development.py
production.py
urls.py
wsgi.py
asgi.py
apps/
users/
models.py
views.py
serializers.py # for DRF
urls.py
admin.py
services.py # business logic separate from views
tests/
test_models.py
test_views.py
products/
orders/
requirements/
base.txt
development.txt
production.txt
Settings are divided by environment and secrets are never committed to the repository:
# config/settings/base.py
from pathlib import Path
import environ
env = environ.Env()
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = env('DJANGO_SECRET_KEY')
DATABASES = {
'default': env.db('DATABASE_URL', default='postgres://localhost/mydb')
}
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': env('REDIS_URL', default='redis://localhost:6379/0'),
'OPTIONS': {'CLIENT_CLASS': 'django_redis.client.DefaultClient'}
}
}
Models and ORM
Django's strength is an expressive ORM with support for complex queries:
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.indexes import GinIndex
class Product(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
attributes = models.JSONField(default=dict)
tags = ArrayField(models.CharField(max_length=50), default=list)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['category', 'is_active']),
GinIndex(fields=['attributes']), # for JSONB search
]
ordering = ['-created_at']
def __str__(self):
return self.name
Complex queries via annotate, aggregate, prefetch_related:
from django.db.models import Count, Avg, Q, Prefetch
# Get categories with count of active products and average price
categories = Category.objects.annotate(
products_count=Count('product', filter=Q(product__is_active=True)),
avg_price=Avg('product__price', filter=Q(product__is_active=True))
).filter(products_count__gt=0).order_by('-products_count')
# N+1 problem solved with prefetch_related
orders = Order.objects.prefetch_related(
Prefetch('items', queryset=OrderItem.objects.select_related('product'))
).select_related('user').filter(status='processing')
Django REST Framework
DRF is the standard for Django APIs. Serializers, ViewSets, pagination:
from rest_framework import serializers, viewsets, permissions, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
class ProductSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True)
class Meta:
model = Product
fields = ['id', 'name', 'slug', 'price', 'category', 'category_name', 'attributes']
read_only_fields = ['slug']
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError('Price must be positive')
return value
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.select_related('category').filter(is_active=True)
serializer_class = ProductSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['category', 'is_active']
search_fields = ['name', 'description']
ordering_fields = ['price', 'created_at']
ordering = ['-created_at']
@action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated])
def favorite(self, request, pk=None):
product = self.get_object()
request.user.favorites.add(product)
return Response({'status': 'added'})
Authentication
JWT via djangorestframework-simplejwt:
# config/urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('api/auth/login/', TokenObtainPairView.as_view()),
path('api/auth/refresh/', TokenRefreshView.as_view()),
]
Custom JWT payload:
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token['email'] = user.email
token['role'] = user.role
return token
Celery for Background Tasks
# tasks.py
from celery import shared_task
from django.core.mail import send_mail
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_order_confirmation(self, order_id: int) -> None:
try:
order = Order.objects.select_related('user').get(id=order_id)
send_mail(
subject=f'Order #{order.id} confirmed',
message=render_email_template('order_confirmation', order),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[order.user.email]
)
except Order.DoesNotExist:
# don't retry — object not found
return
except Exception as exc:
raise self.retry(exc=exc)
Caching
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from functools import wraps
# Low-level caching
def get_popular_products():
cache_key = 'popular_products'
result = cache.get(cache_key)
if result is None:
result = list(Product.objects.filter(is_active=True).order_by('-views')[:10])
cache.set(cache_key, result, timeout=300)
return result
# Invalidation by tags via django-cache-memoize
from cache_memoize import cache_memoize
@cache_memoize(300, extra_verbose_cache_key=True)
def get_category_products(category_id: int, page: int = 1):
# ...
Admin Panel
Django's built-in admin panel saves weeks of work:
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'price', 'category', 'is_active', 'created_at']
list_filter = ['category', 'is_active', 'created_at']
search_fields = ['name', 'slug']
list_editable = ['is_active', 'price']
prepopulated_fields = {'slug': ('name',)}
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
return super().get_queryset(request).select_related('category')
Development Timelines
- Project setup, DB, auth — 3–5 days
- Models + migrations + admin — 1 week
- API on DRF — 1–3 weeks depending on complexity
- Celery + Redis + queues — 3–5 days
- Integrations (payments, email, third-party APIs) — 1–2 weeks
- Tests (pytest-django) — 1 week
Full backend for a corporate website or portal: 5–10 weeks. Django pays for itself with the built-in admin panel — often the client manages content through it without a separate CMS.







