Разработка бэкенда сайта на Python (Django)
Django — «batteries included» в буквальном смысле. ORM, миграции, admin-панель, аутентификация, формы, кеширование, i18n — всё это из коробки, без выбора библиотек и интеграции компонентов. Это делает Django самым быстрым способом запустить полноценный бэкенд, если не нужна экзотика.
Типовая область применения: корпоративные сайты, CMS, порталы, API для SPA и мобильных приложений, системы с богатой бизнес-логикой и ролевой моделью.
Структура проекта
Django-проект состоит из проекта (project) и приложений (apps). Хорошая практика — делать apps небольшими и функционально изолированными:
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 # для DRF
urls.py
admin.py
services.py # бизнес-логика отдельно от views
tests/
test_models.py
test_views.py
products/
orders/
requirements/
base.txt
development.txt
production.txt
Settings разделяем по окружениям и никогда не коммитим секреты в репозиторий:
# 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'}
}
}
Модели и ORM
Сильная сторона Django — выразительный ORM с поддержкой сложных запросов:
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']), # для поиска по JSONB
]
ordering = ['-created_at']
def __str__(self):
return self.name
Сложные запросы через annotate, aggregate, prefetch_related:
from django.db.models import Count, Avg, Q, Prefetch
# Получить категории с количеством активных продуктов и средней ценой
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 проблема решается через 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 — стандарт для API на Django. Сериализаторы, ViewSets, пагинация:
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('Цена должна быть положительной')
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'})
Аутентификация
JWT через 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()),
]
Кастомный payload JWT:
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 для фоновых задач
# 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.id} подтверждён',
message=render_email_template('order_confirmation', order),
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[order.user.email]
)
except Order.DoesNotExist:
# не повторять — объект не найден
return
except Exception as exc:
raise self.retry(exc=exc)
Кеширование
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from functools import wraps
# Низкоуровневое кеширование
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
# Инвалидация по тегам через 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-панель
Встроенная admin-панель Django экономит недели работы:
@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')
Сроки разработки
- Настройка проекта, БД, auth — 3–5 дней
- Модели + миграции + admin — 1 неделя
- API на DRF — 1–3 недели в зависимости от сложности
- Celery + Redis + очереди — 3–5 дней
- Интеграции (платёжки, email, сторонние API) — 1–2 недели
- Тесты (pytest-django) — 1 неделя
Полноценный бэкенд для корпоративного сайта или портала: 5–10 недель. Django окупает себя встроенной admin-панелью — часто клиент управляет контентом через неё без отдельного CMS.







