Construir uma loja virtual moderna costuma ser associado a grandes frameworks de JavaScript e arquiteturas separadas entre frontend e backend. Nesse modelo, surgem camadas extras como APIs REST, gerenciamento de estado no navegador, ferramentas de build e dois ciclos de deploy, o que aumenta custo e manutenção.
Uma alternativa mais simples e produtiva combina Django (backend com renderização no servidor) e HTMX (atributos HTML que disparam requisições e atualizam trechos da página). Essa abordagem mantém páginas rápidas, melhora a indexação (SEO) por padrão e reduz dependências. O resultado é um e-commerce dinâmico, com carrinho e checkout fluidos, além de estoque em tempo real com WebSocket via Django Channels.
Por que HTMX com Django funciona bem em e-commerce
Em e-commerce, as interações frequentes envolvem adicionar itens ao carrinho, ajustar quantidades, validar estoque e finalizar pagamento. Em vez de replicar regras no cliente e no servidor, a lógica fica centralizada no Django, enquanto o HTMX solicita apenas o HTML necessário para atualizar a interface. Isso reduz a chance de inconsistências e simplifica o raciocínio sobre o sistema. Também evita o “peso” de um grande bundle JavaScript e o custo de hidratação no navegador.
HTMX é uma biblioteca pequena que estende HTML com atributos como hx-post, hx-get, hx-target e hx-swap. Esses atributos permitem enviar formulários e trocar apenas um fragmento da página por HTML renderizado no servidor. Django já entrega templates, segurança, sessões, ORM e administração, o que acelera a construção do domínio de loja. A combinação favorece consistência, desempenho e manutenção.
Visão geral da arquitetura e componentes
A arquitetura fica organizada em três blocos principais: renderização HTML no servidor, interações via HTMX e comunicação em tempo real para estoque. O backend em Django concentra regras de negócio, persistência no banco e templates, enquanto o HTMX faz atualizações incrementais sem reescrever a página inteira. Para inventário em tempo real, Django Channels mantém conexões persistentes por WebSocket. Um Redis atua como camada de mensagens (channel layer) para distribuir eventos entre processos.
O carrinho pode ser implementado com sessões, que são dados associados a um usuário mantidos no servidor (e referenciados por cookie). Isso evita manter estado complexo no navegador e reduz a necessidade de endpoints de API. A finalização de compra (checkout) cria pedidos e itens do pedido, reduz estoque e dispara eventos para atualizar telas conectadas. Em produção, a mesma estrutura pode ser escalada com cache e boas práticas de banco.
Preparação do projeto e dependências essenciais
Um projeto típico precisa do Django, do pacote django-htmx para facilitar integração e detecção de requisições HTMX, e do channels para WebSockets. O daphne funciona como servidor ASGI (necessário para WebSocket), e o channels_redis conecta o Channels ao Redis. Para imagens de produtos, Pillow é usado para lidar com arquivos de imagem no Django. Um provedor de pagamento pode ser integrado depois, mas o fluxo de checkout pode ser modelado de forma completa sem depender de API externa.
O conjunto abaixo mostra um exemplo objetivo de instalação e criação do projeto. Ele organiza um “starter kit” pronto para carrinho, checkout e inventário em tempo real. O trecho inclui criação do ambiente virtual e instalação com versões previsíveis. A base resultante já atende o suficiente para evoluir a loja sem reestruturar tudo.
# criar ambiente virtual
python -m venv venv
# ativar ambiente virtual (Linux/macOS)
source venv/bin/activate
# ativar no Windows (PowerShell)
# venv\Scripts\Activate.ps1
# instalar dependências
pip install "django==5.0.*" django-htmx "channels==4.*" daphne "channels_redis==4.*" redis pillow
Configuração do Django para HTMX, sessões e Channels
O Django precisa reconhecer requisições HTMX para retornar fragmentos HTML quando necessário. O django-htmx adiciona um middleware que identifica cabeçalhos HTMX e expõe request.htmx. Para WebSockets, é necessário configurar ASGI_APPLICATION e CHANNEL_LAYERS. O CHANNEL_LAYERS define o backend do Channels, geralmente Redis, para suportar comunicação entre múltiplos processos.
As sessões são essenciais para um carrinho simples e consistente. O Django oferece um backend de sessão padrão em banco de dados, mas Redis também pode ser usado quando necessário. Um identificador como CART_SESSION_ID define a chave do carrinho dentro da sessão. Com isso, o carrinho persiste entre navegações sem exigir autenticação.
# ecommerce_htmx/settings.py (trechos essenciais)
INSTALLED_APPS = [
"daphne", # importante: primeiro para Channels
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_htmx",
"channels",
"shop",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_htmx.middleware.HtmxMiddleware",
]
# sessões (carrinho)
SESSION_ENGINE = "django.contrib.sessions.backends.db"
SESSION_COOKIE_AGE = 86400 * 7 # 7 dias
CART_SESSION_ID = "cart"
# Channels
ASGI_APPLICATION = "ecommerce_htmx.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": [("127.0.0.1", 6379)]},
}
}
Modelagem: categorias, produtos, pedidos e itens de pedido
Um e-commerce precisa representar produtos com preço e estoque, e também registrar pedidos para rastreabilidade. O modelo Product inclui campos como price (decimal para dinheiro) e stock (quantidade disponível). A propriedade in_stock simplifica templates e validações, deixando claro quando a compra é possível. O uso de slug cria URLs amigáveis sem depender do ID numérico.
No checkout, é comum criar um Order e múltiplos OrderItem. O pedido guarda dados do comprador e o total, enquanto os itens registram preço e quantidade no momento da compra. Isso evita que mudanças futuras no preço do produto afetem pedidos antigos. A relação com o produto permite auditoria e relatórios por item vendido.
# shop/models.py
from django.db import models
from django.urls import reverse
class Category(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
class Meta:
verbose_name_plural = "categories"
def __str__(self):
return self.name
class Product(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products")
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
image = models.ImageField(upload_to="products/", blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("shop:product_detail", args=[self.slug])
@property
def in_stock(self):
return self.stock > 0
class Order(models.Model):
full_name = models.CharField(max_length=200)
email = models.EmailField()
address_line1 = models.CharField(max_length=255)
address_line2 = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=120)
postal_code = models.CharField(max_length=20)
total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Pedido #{self.id}"
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name="order_items")
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
return f"{self.product.name} x {self.quantity}"
Carrinho com sessões: como funcionava e como fica com HTMX
Uma abordagem antiga e comum para carrinho é guardar o estado no navegador com JavaScript, replicando preços, somas e regras de estoque no cliente. Isso tende a gerar inconsistências e exige validação duplicada no servidor. Com sessões no Django, o carrinho é um dicionário persistido no servidor e acessado por cookie de sessão. O navegador não precisa “entender” as regras; ele apenas exibe HTML renderizado com valores confiáveis.
O carrinho em sessão guarda apenas o necessário: ID do produto, quantidade e preço capturado no momento da adição. O preço em string evita problemas de serialização com decimal na sessão. Ao iterar o carrinho, o sistema busca produtos no banco e calcula totais com Decimal, evitando erros de ponto flutuante. A lógica fica centralizada e fácil de testar.
# shop/cart.py
from decimal import Decimal
from django.conf import settings
from .models import Product
class Cart:
def __init__(self, request):
self.session = request.session
cart = self.session.get(settings.CART_SESSION_ID)
if cart is None:
cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart
def add(self, product, quantity=1, override_quantity=False):
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {"quantity": 0, "price": str(product.price)}
if override_quantity:
self.cart[product_id]["quantity"] = int(quantity)
else:
self.cart[product_id]["quantity"] += int(quantity)
self.save()
def remove(self, product):
product_id = str(product.id)
if product_id in self.cart:
del self.cart[product_id]
self.save()
def save(self):
self.session.modified = True
def __iter__(self):
product_ids = self.cart.keys()
products = Product.objects.filter(id__in=product_ids)
cart_copia = self.cart.copy()
for product in products:
cart_copia[str(product.id)]["product"] = product
for item in cart_copia.values():
item["price"] = Decimal(item["price"])
item["total_price"] = item["price"] * item["quantity"]
yield item
def __len__(self):
return sum(item["quantity"] for item in self.cart.values())
def get_total_price(self):
return sum(Decimal(item["price"]) * item["quantity"] for item in self.cart.values())
def clear(self):
if settings.CART_SESSION_ID in self.session:
del self.session[settings.CART_SESSION_ID]
self.save()
Views do carrinho com respostas completas e fragmentos HTMX
Um ponto central do HTMX é retornar fragmentos HTML quando a requisição é “HTMX”, e retornar páginas completas quando não é. Isso cria progressive enhancement, ou seja, a aplicação funciona mesmo sem HTMX, mas fica mais dinâmica quando ele está presente. O middleware permite checar request.htmx e decidir se deve renderizar um template parcial. Assim, o mesmo endpoint serve cenários tradicionais e modernos.
O carrinho precisa validar estoque na adição e também no checkout. Na adição, a validação impede inserir quantidade maior do que disponível, retornando erro com status apropriado. Quando HTMX recebe um erro, ele pode exibir a mensagem no alvo configurado. Em remoção, o retorno parcial pode ser uma lista de itens atualizada e um resumo do carrinho, mantendo a interface coerente.
# shop/views.py
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POST
from .cart import Cart
from .models import Product
def product_list(request):
products = Product.objects.all().order_by("-created_at")
return render(request, "shop/product_list.html", {"products": products})
def product_detail(request, slug):
product = get_object_or_404(Product, slug=slug)
return render(request, "shop/product_detail.html", {"product": product})
def cart_detail(request):
cart = Cart(request)
return render(request, "shop/cart_detail.html", {"cart": cart})
@require_POST
def cart_add(request, product_id):
cart = Cart(request)
product = get_object_or_404(Product, id=product_id)
quantity = int(request.POST.get("quantity", 1))
quantity = max(1, quantity)
if product.stock < quantity:
return HttpResponse(
f'<p><strong>Estoque insuficiente.</strong> Disponível: {product.stock}.</p>',
status=400,
)
cart.add(product=product, quantity=quantity)
if request.htmx:
return render(request, "shop/partials/cart_summary.html", {"cart": cart})
return redirect("shop:cart_detail")
@require_POST
def cart_remove(request, product_id):
cart = Cart(request)
product = get_object_or_404(Product, id=product_id)
cart.remove(product)
if request.htmx:
return render(request, "shop/partials/cart_items.html", {"cart": cart})
return redirect("shop:cart_detail")
URLs e estrutura mínima de templates para o fluxo
Rotas claras facilitam manter o projeto e evitam acoplamentos indevidos. A listagem de produtos, detalhe, carrinho e checkout cobrem o núcleo de navegação. Para HTMX, os templates parciais devem ser pequenos e focados, pois serão “trocados” dentro da página. O padrão de partials mantém consistência e reduz duplicação.
O arquivo de URLs do app registra caminhos e nomes de rotas, que são usados pelos templates com url. Isso evita escrever URLs fixas no HTML e reduz quebras quando rotas mudam. A separação entre templates completos e partials favorece uma aplicação legível, onde cada fragmento tem responsabilidade clara.
# shop/urls.py
from django.urls import path
from . import views
app_name = "shop"
urlpatterns = [
path("", views.product_list, name="product_list"),
path("produto/<slug:slug>/", views.product_detail, name="product_detail"),
path("carrinho/", views.cart_detail, name="cart_detail"),
path("carrinho/adicionar/<int:product_id>/", views.cart_add, name="cart_add"),
path("carrinho/remover/<int:product_id>/", views.cart_remove, name="cart_remove"),
path("checkout/", views.checkout, name="checkout"),
]
Template de produto com HTMX: carrinho dinâmico com pouco JavaScript
Em páginas tradicionais, adicionar ao carrinho costuma recarregar a tela inteira ou exigir JavaScript extenso para atualizar contadores e totais. Com HTMX, o formulário envia um POST e troca apenas o resumo do carrinho. O atributo hx-target define o elemento que receberá o HTML retornado, e hx-swap define como o conteúdo será substituído. O servidor devolve um fragmento pronto, evitando lógica de renderização no cliente.
É comum manter um resumo do carrinho visível em um canto da tela. Esse resumo é um template parcial reutilizável e atualizado a cada adição. Caso HTMX não esteja ativo, o mesmo POST pode redirecionar para a página do carrinho, preservando comportamento básico. Essa dualidade reduz fragilidade e aumenta acessibilidade.
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ product.name }}</title>
<script src="https://unpkg.com/htmx.org@1.9.10" defer></script>
</head>
<body>
<main>
<h4>Produto</h4>
<p><strong>Nome:</strong> {{ product.name }}</p>
<p><strong>Preço:</strong> R$ {{ product.price }}</p>
<div id="stock-indicator">
{% include "shop/partials/stock_indicator.html" %}
</div>
<p>{{ product.description }}</p>
<p>O formulário abaixo envia a inclusão no carrinho e atualiza apenas o resumo exibido na tela.</p>
<form
method="post"
action="{% url 'shop:cart_add' product.id %}"
hx-post="{% url 'shop:cart_add' product.id %}"
hx-target="#cart-summary"
hx-swap="outerHTML"
>
{% csrf_token %}
<p>
<label>Quantidade</label>
<input type="number" name="quantity" value="1" min="1" max="{{ product.stock }}">
</p>
<button type="submit" {% if not product.in_stock %}disabled{% endif %}>Adicionar ao carrinho</button>
</form>
</main>
<aside id="cart-summary">
{% include "shop/partials/cart_summary.html" %}
</aside>
</body>
</html>
Parciais do carrinho: resumo e itens removíveis
Templates parciais são pequenos arquivos HTML usados para compor telas e para respostas HTMX. O resumo do carrinho mostra quantidade total e valor total, permitindo feedback imediato. A lista de itens permite remoção com um POST que atualiza apenas a área do carrinho. O mesmo parcial também pode ser usado na página de carrinho completa.
Para remoção, um botão ou formulário pode usar hx-post com alvo na lista inteira. Isso evita manipulação manual de DOM e mantém o servidor como fonte de verdade. Quando um item é removido, o servidor recalcula totais e devolve HTML consistente. Esse padrão reduz erros de arredondamento e divergência de estado.
<!-- templates/shop/partials/cart_summary.html -->
<div id="cart-summary">
<p><strong>Itens no carrinho:</strong> {{ cart|length }}</p>
<p><strong>Total:</strong> R$ {{ cart.get_total_price }}</p>
<p><a href="{% url 'shop:cart_detail' %}" target="_blank" rel="noopener noreferrer">Abrir carrinho</a></p>
</div>
<!-- templates/shop/partials/cart_items.html -->
<div>
<h4>Itens do carrinho</h4>
{% if cart|length == 0 %}
<p>Nenhum item no carrinho.</p>
{% else %}
<p>A lista abaixo apresenta os itens atuais e permite remoção com atualização parcial.</p>
<ul>
{% for item in cart %}
<li>
<p><strong>Produto:</strong> {{ item.product.name }}</p>
<p><strong>Quantidade:</strong> {{ item.quantity }}</p>
<p><strong>Subtotal:</strong> R$ {{ item.total_price }}</p>
<form
method="post"
action="{% url 'shop:cart_remove' item.product.id %}"
hx-post="{% url 'shop:cart_remove' item.product.id %}"
hx-target="#cart-items"
hx-swap="outerHTML"
>
{% csrf_token %}
<button type="submit">Remover</button>
</form>
</li>
{% endfor %}
</ul>
<div>
{% include "shop/partials/cart_summary.html" %}
</div>
{% endif %}
</div>
Página do carrinho: composição com partials
A página do carrinho serve como visão completa e também como fallback quando HTMX não é usado. Ela inclui a lista de itens e o resumo, ambos reaproveitando os mesmos partials. Esse reuso garante que a informação exibida seja idêntica em atualizações parciais e em páginas completas. Em uma base maior, isso reduz divergências visuais e lógicas.
Além disso, o carrinho costuma ser o ponto de transição para o checkout. Por isso, manter um link claro para finalização é útil, sem acoplar lógica de pagamento nesse momento. A página também é um bom local para exibir mensagens de validação, como estoque indisponível após mudanças em tempo real. A consistência do HTML retornado simplifica essa comunicação.
<!-- templates/shop/cart_detail.html -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Carrinho</title>
<script src="https://unpkg.com/htmx.org@1.9.10" defer></script>
</head>
<body>
<main>
<h4>Carrinho</h4>
<div id="cart-items">
{% include "shop/partials/cart_items.html" %}
</div>
<p><a href="{% url 'shop:checkout' %}" target="_blank" rel="noopener noreferrer">Ir para checkout</a></p>
</main>
</body>
</html>
Checkout: formulário, validação e criação do pedido
O checkout precisa capturar dados essenciais e transformar o carrinho em um pedido persistido. Uma forma clara de fazer isso é usar um ModelForm, que é um formulário Django baseado em um model, com validações automáticas. No POST, se o formulário for válido, o sistema cria o pedido e seus itens e calcula o total com base no carrinho. O total e o preço por item ficam registrados para consistência histórica.
O passo mais sensível é a redução de estoque, pois envolve concorrência: duas compras podem tentar consumir o mesmo estoque. Em cenários reais, o ideal é usar transaction.atomic e travas de linha com select_for_update para evitar estoque negativo. Mesmo em um starter kit, essa preocupação deve aparecer na implementação, pois evita pedidos impossíveis. Após a conclusão, o carrinho é limpo e um parcial de sucesso pode ser retornado via HTMX.
# shop/forms.py
from django import forms
from .models import Order
class CheckoutForm(forms.ModelForm):
class Meta:
model = Order
fields = [
"full_name",
"email",
"address_line1",
"address_line2",
"city",
"postal_code",
]
# shop/views.py (checkout)
from decimal import Decimal
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import redirect, render
from .cart import Cart
from .forms import CheckoutForm
from .models import OrderItem, Product
def checkout(request):
cart = Cart(request)
if len(cart) == 0:
return HttpResponse("<p>Carrinho vazio.</p>", status=400)
if request.method == "POST":
form = CheckoutForm(request.POST)
if form.is_valid():
with transaction.atomic():
order = form.save(commit=False)
order.total = Decimal("0.00")
order.save()
# bloqueia produtos do carrinho para evitar concorrência simples
product_ids = [item["product"].id for item in cart]
produtos = Product.objects.select_for_update().filter(id__in=product_ids)
produtos_map = {p.id: p for p in produtos}
for item in cart:
product = produtos_map[item["product"].id]
quantidade = int(item["quantity"])
if product.stock < quantidade:
raise ValueError("Estoque insuficiente durante o checkout.")
OrderItem.objects.create(
order=order,
product=product,
price=item["price"],
quantity=quantidade,
)
product.stock -= quantidade
product.save(update_fields=["stock"])
order.total += item["price"] * quantidade
order.save(update_fields=["total"])
# notifica atualização de estoque (tempo real)
channel_layer = get_channel_layer()
for item in cart:
product = item["product"]
async_to_sync(channel_layer.group_send)(
"inventory",
{
"type": "inventory_update",
"product_id": product.id,
"stock": Product.objects.get(id=product.id).stock,
},
)
cart.clear()
if request.htmx:
return render(request, "shop/partials/order_success.html", {"order": order})
return redirect("shop:product_list")
else:
form = CheckoutForm()
return render(request, "shop/checkout.html", {"cart": cart, "form": form})
Templates do checkout com HTMX e resposta de sucesso
No checkout, o HTMX pode enviar o formulário e substituir apenas uma área de conteúdo por uma mensagem de confirmação. Isso reduz “piscadas” de navegação e mantém a experiência fluida. O HTML de sucesso também pode incluir um resumo do pedido, como número e total. Quando HTMX não participa, o comportamento padrão ainda funciona com redirecionamento.
Também é útil mostrar o total atual do carrinho ao lado do formulário. Como o valor vem do servidor, não há risco de exibir total divergente por manipulação no navegador. O formulário pode ser simples e direto, com validação do Django cuidando de campos obrigatórios. A mensagem final encerra o fluxo de forma clara e fechada.
<!-- templates/shop/checkout.html -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Checkout</title>
<script src="https://unpkg.com/htmx.org@1.9.10" defer></script>
</head>
<body>
<main>
<h4>Checkout</h4>
<p><strong>Total atual:</strong> R$ {{ cart.get_total_price }}</p>
<div id="checkout-area">
<p>O formulário abaixo registra os dados do pedido e conclui a compra no servidor.</p>
<form
method="post"
action="{% url 'shop:checkout' %}"
hx-post="{% url 'shop:checkout' %}"
hx-target="#checkout-area"
hx-swap="innerHTML"
>
{% csrf_token %}
<p>{{ form.full_name.label_tag }} {{ form.full_name }}</p>
<p>{{ form.email.label_tag }} {{ form.email }}</p>
<p>{{ form.address_line1.label_tag }} {{ form.address_line1 }}</p>
<p>{{ form.address_line2.label_tag }} {{ form.address_line2 }}</p>
<p>{{ form.city.label_tag }} {{ form.city }}</p>
<p>{{ form.postal_code.label_tag }} {{ form.postal_code }}</p>
<button type="submit">Confirmar pedido</button>
</form>
</div>
</main>
</body>
</html>
<!-- templates/shop/partials/order_success.html -->
<div>
<h4>Pedido confirmado</h4>
<p><strong>Número do pedido:</strong> {{ order.id }}</p>
<p><strong>Total:</strong> R$ {{ order.total }}</p>
<p>O pedido foi registrado e o estoque foi atualizado.</p>
</div>
Inventário em tempo real com WebSocket, Channels e Redis
Atualização de estoque em tempo real evita frustração quando um item esgota enquanto a página está aberta. O WebSocket mantém um canal aberto entre navegador e servidor, diferente do HTTP tradicional que abre e fecha conexões por requisição. O Django Channels fornece consumidores assíncronos, que recebem eventos e enviam mensagens para os clientes conectados. O grupo “inventory” permite broadcast para todos os navegadores que acompanham estoque.
Quando um pedido é finalizado, o servidor reduz o estoque e envia um evento ao grupo. Os clientes recebem o ID do produto e o novo estoque e podem atualizar o indicador na tela. Uma estratégia simples é disparar uma requisição HTMX para buscar um fragmento HTML atualizado do indicador. Isso mantém renderização no servidor e evita duplicar regras no JavaScript.
# shop/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class InventoryConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.channel_layer.group_add("inventory", self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard("inventory", self.channel_name)
async def inventory_update(self, event):
await self.send(text_data=json.dumps({
"product_id": event["product_id"],
"stock": event["stock"],
}))
# shop/routing.py
from django.urls import re_path
from .consumers import InventoryConsumer
websocket_urlpatterns = [
re_path(r"ws/inventory/$", InventoryConsumer.as_asgi()),
]
# ecommerce_htmx/asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from shop.routing import websocket_urlpatterns
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ecommerce_htmx.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
),
})
Indicador de estoque com parcial e endpoint para atualização
Um indicador de estoque costuma exibir “disponível”, “últimas unidades” ou “esgotado”. Essa lógica deve ficar no servidor para ser reutilizada em qualquer tela e para refletir exatamente o valor no banco. Um template parcial de estoque recebe um produto e decide a mensagem. Quando chega um evento via WebSocket, o cliente pede ao servidor o HTML do indicador e substitui apenas aquela área.
Para isso, um endpoint simples retorna o parcial com o produto atualizado. A resposta é pequena e rápida, ideal para atualizações frequentes. Em páginas de detalhe, o indicador pode estar em um container com ID estável, facilitando o swap. Esse padrão também funciona em listagens com vários produtos, usando IDs por produto.
# shop/views.py (endpoint de estoque)
from django.shortcuts import get_object_or_404, render
from .models import Product
def stock_partial(request, product_id):
product = get_object_or_404(Product, id=product_id)
return render(request, "shop/partials/stock_indicator.html", {"product": product})
# shop/urls.py (acrescentar rota)
from django.urls import path
from . import views
app_name = "shop"
urlpatterns = [
path("", views.product_list, name="product_list"),
path("produto/<slug:slug>/", views.product_detail, name="product_detail"),
path("carrinho/", views.cart_detail, name="cart_detail"),
path("carrinho/adicionar/<int:product_id>/", views.cart_add, name="cart_add"),
path("carrinho/remover/<int:product_id>/", views.cart_remove, name="cart_remove"),
path("checkout/", views.checkout, name="checkout"),
path("estoque/<int:product_id>/", views.stock_partial, name="stock_partial"),
]
<!-- templates/shop/partials/stock_indicator.html -->
<div id="stock-indicator">
{% if product.stock > 10 %}
<p><strong>Estoque:</strong> disponível</p>
{% elif product.stock > 0 %}
<p><strong>Estoque:</strong> últimas unidades ({{ product.stock }})</p>
{% else %}
<p><strong>Estoque:</strong> esgotado</p>
{% endif %}
</div>
JavaScript mínimo: recebendo WebSocket e pedindo HTML ao servidor
Mesmo usando HTMX, algum JavaScript pode ser necessário para WebSocket, pois ele não é HTTP tradicional. O script abre conexão com o endpoint de inventário e escuta mensagens. Ao receber um evento, ele dispara uma requisição HTMX para buscar o parcial atualizado. Assim, a atualização visual permanece alinhada ao template do servidor.
Para evitar erros de protocolo, é importante usar wss quando a aplicação estiver em HTTPS e ws quando estiver em HTTP. O script também pode ser carregado somente em páginas que exibem estoque. Em caso de falha, a loja continua funcional, apenas sem atualização em tempo real.
<script>
(function () {
var protocolo = window.location.protocol === "https:" ? "wss" : "ws";
var urlSocket = protocolo + "://" + window.location.host + "/ws/inventory/";
var socketInventario = new WebSocket(urlSocket);
socketInventario.onmessage = function (evento) {
var dados = JSON.parse(evento.data);
if (!dados.product_id) {
return;
}
// busca o HTML atualizado do estoque e troca o conteúdo
htmx.ajax("GET", "/estoque/" + dados.product_id + "/", {
target: "#stock-indicator",
swap: "outerHTML"
});
};
})();
</script>
Benchmarks e o que muda na prática
Em uma arquitetura SPA (aplicação de página única), o navegador baixa um grande bundle JavaScript, executa, faz chamadas à API e então renderiza. Esse “encadeamento” costuma aumentar o tempo até a primeira interação, especialmente em conexões lentas. Com Django + HTMX, a primeira página já chega pronta em HTML e as interações trocam fragmentos pequenos. O custo de parse e execução de JavaScript diminui, e o tráfego pode ser menor em operações simples.
Em números típicos desse tipo de comparação, a carga inicial tende a ser mais rápida no servidor-renderizado e os pacotes menores. Operações como “adicionar ao carrinho” viram uma requisição pequena que retorna um fragmento HTML, muitas vezes em dezenas de milissegundos. A contrapartida é maior dependência do servidor para renderização e mais requisições HTML, o que exige atenção a cache e eficiência no ORM. Em e-commerce, esse trade-off costuma ser vantajoso por priorizar consistência e simplicidade.
Cuidados de produção: cache, banco, segurança e escala
Em produção, desempenho e consistência dependem de alguns cuidados básicos. Cache de listagens e fragmentos reduz renderizações repetidas, e o Redis pode ser usado tanto para cache quanto para sessions e channel layer. No banco, índices em campos consultados com frequência, como slug e estoque, reduzem latência. No ORM, select_related e prefetch_related ajudam a evitar o problema N+1, que é quando múltiplas consultas pequenas degradam o tempo total.
Segurança inclui proteção de CSRF nos formulários e limitação de abuso em operações de carrinho. O Django já entrega proteção CSRF por padrão, mas endpoints precisam manter o token em formulários HTMX. Rate limiting pode ser aplicado para reduzir tentativas automatizadas de “reservar” estoque via carrinho. Para escala, múltiplos processos ASGI com Redis suportam muitos WebSockets, e a renderização em servidor pode ser acelerada com cache e páginas bem segmentadas em partials.
Conclusão
A combinação de Django e HTMX permite construir um e-commerce dinâmico com menos camadas e menos complexidade operacional. O carrinho em sessão mantém o estado no servidor de forma confiável, enquanto o HTMX atualiza a interface com fragmentos HTML pequenos e consistentes. O checkout cria pedidos de maneira transacional, reduz estoque com segurança e mantém histórico de preços por item. Com Django Channels, o inventário em tempo real complementa a experiência ao sincronizar disponibilidade entre telas abertas.
Esse modelo substitui a necessidade de um grande framework de frontend por uma abordagem centrada em HTML e renderização no servidor, sem abrir mão de interatividade moderna. A manutenção tende a ser mais simples por concentrar regras em um único código-base e reduzir duplicação entre cliente e servidor. Com cache, boas práticas de ORM e infraestrutura adequada, a solução suporta tráfego significativo mantendo baixo custo de complexidade. O resultado é um kit inicial sólido e coerente para carrinho, checkout e estoque em tempo real.