Django is a batteries-included Python web framework. This guide covers advanced patterns beyond the tutorial — ORM optimization, DRF APIs, signals, and production deployment.

Project Structure for Scale

  myproject/
├── config/
│   ├── settings/
│   │   ├── base.py
│   │   ├── development.py
│   │   └── production.py
│   ├── urls.py
│   └── wsgi.py
├── apps/
│   ├── users/
│   │   ├── models.py
│   │   ├── views.py
│   │   ├── serializers.py
│   │   ├── services.py
│   │   └── tests/
│   └── orders/
└── manage.py
  

Split settings by environment — never commit production secrets.

Custom Model Managers and QuerySets

  # apps/users/models.py
from django.db import models

class ActiveUserManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_active=True)

class UserQuerySet(models.QuerySet):
    def with_orders(self):
        return self.prefetch_related('orders')

    def admins(self):
        return self.filter(role='admin')

class User(models.Model):
    email = models.EmailField(unique=True)
    name = models.CharField(max_length=100)
    role = models.CharField(max_length=20, default='user')
    is_active = models.BooleanField(default=True)

    objects = models.Manager()
    active = ActiveUserManager()
    queryset = UserQuerySet.as_manager()

# Usage
User.active.admins().with_orders()
  

Avoid N+1 queries:

  # ❌ N+1 — one query per order's user
orders = Order.objects.all()
for order in orders:
    print(order.user.email)  # hits DB each iteration

# ✅ Two queries total
orders = Order.objects.select_related('user').all()
for order in orders:
    print(order.user.email)

# Many-to-many / reverse FK
users = User.objects.prefetch_related('orders__items').all()
  

Use django-debug-toolbar in development to detect N+1 queries.

Signals

Decouple side effects from business logic:

  # apps/users/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import User

@receiver(post_save, sender=User)
def send_welcome_email(sender, instance, created, **kwargs):
    if created:
        from .tasks import send_email
        send_email.delay(
            to=instance.email,
            subject='Welcome!',
            body=f'Hello {instance.name}',
        )
  

Register in apps.py:

  class UsersConfig(AppConfig):
    def ready(self):
        import apps.users.signals  # noqa
  

Prefer explicit service functions over excessive signals — signals can make flow hard to trace.

Custom Middleware

  # config/middleware.py
import time
import logging

logger = logging.getLogger(__name__)

class RequestTimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start = time.perf_counter()
        response = self.get_response(request)
        duration = time.perf_counter() - start
        logger.info(
            '%s %s %.3fs',
            request.method,
            request.path,
            duration,
        )
        response['X-Request-Duration'] = f'{duration:.3f}'
        return response
  

Register in settings:

  MIDDLEWARE = [
    'config.middleware.RequestTimingMiddleware',
    # ...
]
  

Django REST Framework (DRF)

  pip install djangorestframework djangorestframework-simplejwt
  
  # apps/users/serializers.py
from rest_framework import serializers
from .models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'email', 'name', 'role']
        read_only_fields = ['id']

# apps/users/views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User
from .serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.active.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]

    @action(detail=True, methods=['post'])
    def deactivate(self, request, pk=None):
        user = self.get_object()
        user.is_active = False
        user.save()
        return Response({'status': 'deactivated'})
  

URL routing:

  from rest_framework.routers import DefaultRouter
from apps.users.views import UserViewSet

router = DefaultRouter()
router.register('users', UserViewSet)
urlpatterns = router.urls
  

Authentication with JWT

  # config/urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('api/token/', TokenObtainPairView.as_view()),
    path('api/token/refresh/', TokenRefreshView.as_view()),
    path('api/', include(router.urls)),
]
  

Celery for Background Tasks

  pip install celery redis
  
  # config/celery.py
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.production')
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

# apps/orders/tasks.py
from celery import shared_task

@shared_task(bind=True, max_retries=3)
def process_order(self, order_id):
    from apps.orders.services import fulfill_order
    try:
        fulfill_order(order_id)
    except Exception as exc:
        self.retry(exc=exc, countdown=60)
  
  # settings
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
  

Caching

  from django.core.cache import cache
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # 15 minutes
def product_list(request):
    products = cache.get('products:all')
    if products is None:
        products = list(Product.objects.all().values())
        cache.set('products:all', products, 900)
    return JsonResponse({'products': products})
  

Configure Redis cache backend in production settings.

Testing

  # apps/users/tests/test_views.py
import pytest
from django.urls import reverse
from rest_framework.test import APIClient
from apps.users.models import User

@pytest.fixture
def api_client():
    return APIClient()

@pytest.fixture
def auth_client(api_client):
    user = User.objects.create_user(email='[email protected]', password='pass')
    api_client.force_authenticate(user=user)
    return api_client

@pytest.mark.django_db
def test_list_users(auth_client):
    User.objects.create(email='[email protected]', name='Alice')
    response = auth_client.get('/api/users/')
    assert response.status_code == 200
    assert len(response.data) >= 1
  

Production Checklist

  • DEBUG = False in production
  • ALLOWED_HOSTS configured
  • PostgreSQL (not SQLite) for production
  • Static files via WhiteNoise or CDN
  • Gunicorn + Nginx (or uvicorn for ASGI)
  • Environment variables for secrets (django-environ)
  • Database connection pooling (django-db-connection-pool)
  • Migrations run in CI/CD before deploy

Django’s convention-over-configuration accelerates development — these advanced patterns keep applications maintainable at scale.