HTMX + Django para E-commerce Moderno: Carrinho, Checkout e Estoque em Tempo Real com Alto Desempenho

Published on: 2026-01-27
Post image
pt htmx-django django-ecommerce htmx-ecommerce django-htmx-tutorial ecommerce-com-django carrinho-de-compras-django checkout-django estoque-em-tempo-real-django django-channels-websocket htmx-real-time django-sem-react ecommerce-sem-javascript js

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.