Formulários são um dos pontos mais sensíveis de uma aplicação web, porque concentram entrada de dados, validação, segurança e integração com banco. No ecossistema do Django, os Django Forms representam uma camada completa para criar formulários confiáveis, reduzindo erros comuns como campos inválidos, senhas fracas e uploads inseguros.
Um formulário bem construído no Django reúne geração de HTML, validação, mensagens de erro e tratamento de arquivos em um fluxo previsível. Essa abordagem evita validações manuais longas e frágeis, além de padronizar o que acontece quando os dados chegam ao servidor. O resultado é um caminho mais sólido entre o envio do usuário e a persistência ou processamento final.
O que são Django Forms e por que resolvem problemas reais
Django Forms são classes Python que descrevem campos e regras de validação de um formulário. Cada campo sabe como validar seu próprio tipo, como e-mail, texto, número e data. Quando um formulário recebe dados, o Django executa as validações e registra erros de forma estruturada. Além disso, a classe também consegue renderizar os campos em HTML, reduzindo inconsistências no front-end.
Essa estrutura evita repetir lógica de verificação em múltiplos lugares. Em vez de validar manualmente cada entrada com condicionais, o formulário centraliza as regras e expõe resultados padronizados. O método is_valid() indica se os dados passaram por todas as validações. Em caso de sucesso, os valores ficam disponíveis em cleaned_data, já normalizados.
Estrutura básica de um formulário com validação automática
Um formulário simples define campos e delega ao Django a maior parte da validação. Um CharField representa texto, um EmailField valida formato de e-mail, e widgets controlam como o campo aparece no HTML. O parâmetro required define obrigatoriedade, e limites como min_length e max_length aplicam regras comuns. A combinação desses elementos cobre uma grande parte dos casos sem código extra.
O exemplo a seguir mostra um formulário de contato completo e inclui validações personalizadas. A validação por campo acontece em métodos clean_nome_do_campo. A validação do formulário inteiro acontece no método clean(), adequado para regras que dependem de mais de um campo. Essas validações retornam dados normalizados ou levantam ValidationError para registrar erros.
from django import forms
class FormularioContato(forms.Form):
nome = forms.CharField(
max_length=100,
required=True,
widget=forms.TextInput(attrs={
"placeholder": "Nome completo"
})
)
email = forms.EmailField(
required=True,
widget=forms.EmailInput(attrs={
"placeholder": "nome@dominio.com"
})
)
assunto = forms.CharField(
max_length=200,
required=True,
widget=forms.TextInput(attrs={
"placeholder": "Assunto"
})
)
mensagem = forms.CharField(
required=True,
widget=forms.Textarea(attrs={
"rows": 5,
"placeholder": "Mensagem"
})
)
def clean_nome(self):
nome = self.cleaned_data.get("nome", "")
if any(caractere.isdigit() for caractere in nome):
raise forms.ValidationError("O nome não pode conter números.")
return nome.strip().title()
def clean(self):
dados = super().clean()
email = dados.get("email")
if email and email.lower().endswith("@spam.com"):
raise forms.ValidationError("Este domínio de e-mail não é permitido.")
return dados
Uso do formulário na view com fluxo correto de validação
Uma view é a função que recebe a requisição e decide o que renderizar ou para onde redirecionar. Quando o método HTTP é POST, o formulário é instanciado com request.POST, que contém os dados enviados. O método is_valid() executa toda a validação e prepara cleaned_data. Em caso de sucesso, o fluxo típico termina com redirecionamento para evitar reenvios duplicados.
Quando a validação falha, o formulário retorna com erros vinculados aos campos, permitindo exibir mensagens específicas. O Django também oferece mensagens temporárias via django.contrib.messages, úteis para informar sucesso ou falhas gerais. O padrão Post/Redirect/Get evita que um refresh do navegador reencontre o POST e dispare o processamento novamente. Esse padrão é especialmente importante em operações que salvam dados.
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import FormularioContato
def contato_view(request):
if request.method == "POST":
form = FormularioContato(request.POST)
if form.is_valid():
dados = form.cleaned_data
# Exemplo de processamento:
# enviar_email_contato(
# nome=dados["nome"],
# email=dados["email"],
# assunto=dados["assunto"],
# mensagem=dados["mensagem"],
# )
messages.success(request, "Mensagem enviada com sucesso.")
return redirect("contato")
messages.error(request, "Existem erros no formulário.")
else:
form = FormularioContato()
return render(request, "contato.html", {"form": form})
Renderização no template e exibição correta de erros
O template é responsável por mostrar campos e mensagens de erro. O Django inclui proteção contra CSRF, um ataque que explora requisições forjadas, e isso exige o token csrf_token no formulário. A renderização pode ser automática, mas a abordagem manual dá mais controle sobre layout e mensagens. Erros por campo ficam em form.campo.errors, e erros gerais do formulário ficam em form.non_field_errors.
O exemplo a seguir mostra uma renderização manual essencial e completa. Cada bloco exibe o campo e seus erros logo abaixo. O atributo novalidate desativa validação nativa do navegador, quando se deseja depender apenas do servidor. Mesmo quando há validação no front-end, a validação do servidor continua obrigatória por segurança.
<form method="post" novalidate>
{% csrf_token %}
<div>
<label for="{{ form.nome.id_for_label }}">Nome</label>
{{ form.nome }}
{% if form.nome.errors %}
<div>{{ form.nome.errors }}</div>
{% endif %}
</div>
<div>
<label for="{{ form.email.id_for_label }}">E-mail</label>
{{ form.email }}
{% if form.email.errors %}
<div>{{ form.email.errors }}</div>
{% endif %}
</div>
<div>
<label for="{{ form.assunto.id_for_label }}">Assunto</label>
{{ form.assunto }}
{% if form.assunto.errors %}
<div>{{ form.assunto.errors }}</div>
{% endif %}
</div>
<div>
<label for="{{ form.mensagem.id_for_label }}">Mensagem</label>
{{ form.mensagem }}
{% if form.mensagem.errors %}
<div>{{ form.mensagem.errors }}</div>
{% endif %}
</div>
{% if form.non_field_errors %}
<div>{{ form.non_field_errors }}</div>
{% endif %}
<button type="submit">Enviar</button>
</form>
Validação avançada em cadastro de usuário com regras mais rígidas
Cadastros costumam exigir regras que vão além do básico, como unicidade e força de senha. Unicidade significa que o valor não pode existir previamente no banco, como nome de usuário e e-mail. Força de senha é um conjunto de condições, como presença de letras maiúsculas, minúsculas e números. Também é comum exigir confirmação de senha, validada em clean() por envolver dois campos.
O exemplo abaixo implementa essas regras em um formulário comum. A consulta ao banco utiliza o modelo User do Django, e o filtro __iexact ignora diferenças de maiúsculas e minúsculas. O método clean_password garante os critérios mínimos antes do processamento. Ao final, os valores são normalizados para reduzir variações, como e-mails com letras maiúsculas.
from django import forms
from django.contrib.auth.models import User
class FormularioCadastroUsuario(forms.Form):
username = forms.CharField(min_length=3, max_length=20)
email = forms.EmailField()
password = forms.CharField(min_length=8, widget=forms.PasswordInput())
confirm_password = forms.CharField(widget=forms.PasswordInput())
def clean_username(self):
username = (self.cleaned_data.get("username") or "").strip()
if not username.isalnum():
raise forms.ValidationError("O usuário deve conter apenas letras e números.")
if User.objects.filter(username__iexact=username).exists():
raise forms.ValidationError("Este usuário já está em uso.")
return username.lower()
def clean_email(self):
email = (self.cleaned_data.get("email") or "").strip().lower()
if User.objects.filter(email__iexact=email).exists():
raise forms.ValidationError("Já existe uma conta com este e-mail.")
return email
def clean_password(self):
senha = self.cleaned_data.get("password") or ""
if not any(c.isupper() for c in senha):
raise forms.ValidationError("A senha deve conter ao menos uma letra maiúscula.")
if not any(c.islower() for c in senha):
raise forms.ValidationError("A senha deve conter ao menos uma letra minúscula.")
if not any(c.isdigit() for c in senha):
raise forms.ValidationError("A senha deve conter ao menos um número.")
senhas_comuns = {"password", "12345678", "qwerty123"}
if senha.lower() in senhas_comuns:
raise forms.ValidationError("A senha informada é muito comum.")
return senha
def clean(self):
dados = super().clean()
senha = dados.get("password")
confirmacao = dados.get("confirm_password")
if senha and confirmacao and senha != confirmacao:
raise forms.ValidationError("As senhas não coincidem.")
return dados
Upload de arquivos com validação de extensão, tamanho e tipo
Uploads precisam de validações mais rígidas, porque envolvem armazenamento e risco de conteúdo indevido. Extensão é o sufixo do nome do arquivo, como pdf e jpg, e pode ser verificada por validadores como FileExtensionValidator. Tamanho deve ser limitado para evitar consumo excessivo de disco e processamento. Tipo de conteúdo (content_type) é um metadado enviado pelo cliente e ajuda a filtrar, mas não deve ser a única barreira.
O formulário a seguir inclui arquivo único e múltiplos arquivos. O campo múltiplo usa ClearableFileInput com atributo multiple, e a view recebe a lista com request.FILES.getlist. A validação de tamanho é extraída para uma função reutilizável. Isso cria um padrão consistente para vários campos e reduz repetição.
from django import forms
from django.core.validators import FileExtensionValidator
from django.core.exceptions import ValidationError
def validar_tamanho_arquivo(arquivo, max_mb):
max_bytes = max_mb * 1024 * 1024
if arquivo.size > max_bytes:
raise ValidationError(f"O arquivo excede {max_mb}MB.")
class FormularioCandidatura(forms.Form):
nome_completo = forms.CharField(max_length=100)
email = forms.EmailField()
telefone = forms.CharField(max_length=15)
curriculo = forms.FileField(
validators=[FileExtensionValidator(
allowed_extensions=["pdf", "doc", "docx"],
message="Apenas PDF, DOC e DOCX são permitidos."
)],
widget=forms.FileInput(attrs={
"accept": ".pdf,.doc,.docx"
})
)
carta_apresentacao = forms.FileField(
required=False,
validators=[FileExtensionValidator(
allowed_extensions=["pdf"],
message="Apenas PDF é permitido."
)],
widget=forms.FileInput(attrs={
"accept": ".pdf"
})
)
imagens_portfolio = forms.FileField(
required=False,
widget=forms.ClearableFileInput(attrs={
"accept": "image/*",
"multiple": True
})
)
def clean_curriculo(self):
curriculo = self.cleaned_data.get("curriculo")
if curriculo:
validar_tamanho_arquivo(curriculo, max_mb=5)
tipos = {
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
}
if curriculo.content_type not in tipos:
raise ValidationError("Tipo de arquivo inválido.")
return curriculo
def clean_carta_apresentacao(self):
carta = self.cleaned_data.get("carta_apresentacao")
if carta:
validar_tamanho_arquivo(carta, max_mb=5)
if carta.content_type != "application/pdf":
raise ValidationError("Tipo de arquivo inválido.")
return carta
Tratamento de múltiplos arquivos na view com armazenamento consistente
Uploads múltiplos exigem um loop para validar e salvar cada arquivo. O método getlist retorna todos os arquivos enviados para o mesmo nome de campo. Além de validar extensão e tamanho, uma checagem de content_type reduz o risco de arquivos disfarçados. A gravação pode usar FileSystemStorage, que abstrai o caminho e a escrita no disco.
O exemplo a seguir cria uma pasta única por candidatura usando um identificador simples. Isso evita colisão de nomes e facilita organização. O código também separa arquivos válidos e registra erros sem interromper a requisição inteira. Em sistemas reais, a persistência em banco normalmente relaciona cada caminho salvo ao registro da candidatura.
import uuid
from django.shortcuts import render, redirect
from django.contrib import messages
from django.core.files.storage import FileSystemStorage
from .forms import FormularioCandidatura
def candidatura_view(request):
if request.method == "POST":
form = FormularioCandidatura(request.POST, request.FILES)
if form.is_valid():
curriculo = form.cleaned_data["curriculo"]
carta = form.cleaned_data.get("carta_apresentacao")
imagens = request.FILES.getlist("imagens_portfolio")
extensoes_validas = {"png", "jpg", "jpeg"}
imagens_validas = []
for imagem in imagens:
ext = imagem.name.rsplit(".", 1)[-1].lower()
if ext not in extensoes_validas:
messages.error(request, f"Tipo inválido em {imagem.name}.")
continue
if imagem.size > 2 * 1024 * 1024:
messages.error(request, f"{imagem.name} excede 2MB.")
continue
if not (imagem.content_type or "").startswith("image/"):
messages.error(request, f"{imagem.name} não é uma imagem válida.")
continue
imagens_validas.append(imagem)
storage = FileSystemStorage(location="media/candidaturas")
protocolo = uuid.uuid4().hex[:8]
pasta = f"candidaturas/{protocolo}"
caminho_curriculo = storage.save(f"{pasta}/curriculo_{curriculo.name}", curriculo)
caminho_carta = None
if carta:
caminho_carta = storage.save(f"{pasta}/carta_{carta.name}", carta)
caminhos_imagens = []
for i, imagem in enumerate(imagens_validas, start=1):
caminho = storage.save(f"{pasta}/portfolio_{i}_{imagem.name}", imagem)
caminhos_imagens.append(caminho)
messages.success(request, f"Candidatura enviada. Protocolo: {protocolo}")
return redirect("candidatura")
messages.error(request, "Existem erros no formulário.")
else:
form = FormularioCandidatura()
return render(request, "candidatura.html", {"form": form})
Pré-visualização de arquivos no front-end sem comprometer a validação do servidor
Uma pré-visualização melhora a experiência, mas não substitui validação do back-end. Para imagens, o navegador pode ler o arquivo local usando FileReader e gerar uma URL em base64. Para documentos, uma prévia simples pode mostrar nome e tamanho. Esse comportamento é totalmente opcional e deve falhar de modo seguro quando o arquivo não é imagem.
O trecho abaixo mostra pré-visualização de imagens de portfólio e metadados de um currículo. O código evita dependências externas e usa apenas eventos do DOM. A validação de tamanho no front-end pode impedir seleção de arquivos grandes, mas o limite definitivo deve continuar no servidor. A renderização do HTML é minimalista para manter previsibilidade.
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div id="preview-curriculo"></div>
<div id="preview-portfolio"></div>
<button type="submit">Enviar</button>
</form>
<script>
document.addEventListener("DOMContentLoaded", function () {
const inputCurriculo = document.querySelector('input[name="curriculo"]');
const inputPortfolio = document.querySelector('input[name="imagens_portfolio"]');
const previewCurriculo = document.getElementById("preview-curriculo");
const previewPortfolio = document.getElementById("preview-portfolio");
if (inputCurriculo) {
inputCurriculo.addEventListener("change", function (e) {
const arquivo = e.target.files && e.target.files[0];
previewCurriculo.innerHTML = "";
if (!arquivo) return;
const tamanhoMB = (arquivo.size / (1024 * 1024)).toFixed(2);
previewCurriculo.innerHTML = `<p>Arquivo selecionado: <strong>${arquivo.name}</strong> (${tamanhoMB} MB)</p>`;
});
}
if (inputPortfolio) {
inputPortfolio.addEventListener("change", function (e) {
const arquivos = Array.from(e.target.files || []);
previewPortfolio.innerHTML = "";
arquivos.forEach((arquivo) => {
if (!arquivo.type.startsWith("image/")) return;
const leitor = new FileReader();
leitor.onload = function (ev) {
const img = document.createElement("img");
img.src = ev.target.result;
img.alt = "Prévia do portfólio";
img.width = 160;
const info = document.createElement("p");
const tamanhoMB = (arquivo.size / (1024 * 1024)).toFixed(2);
info.textContent = `${arquivo.name} (${tamanhoMB} MB)`;
previewPortfolio.appendChild(img);
previewPortfolio.appendChild(info);
};
leitor.readAsDataURL(arquivo);
});
});
}
});
</script>
Depuração: por que um formulário não valida e como identificar a causa
Quando is_valid() retorna falso, o motivo sempre fica registrado em form.errors. Esse dicionário mapeia campo para lista de mensagens, e também pode ser serializado em JSON. Erros de formulário inteiro ficam em non_field_errors(), comuns em validações cruzadas. A depuração correta começa verificando o conteúdo real recebido no POST e em FILES.
As falhas mais comuns incluem ausência de csrf_token, falta de enctype em formulários com arquivo e esquecimento de passar request.FILES para o formulário. Outra fonte frequente é divergência de nomes entre o input HTML e o campo do formulário. O bloco a seguir mostra um padrão simples para imprimir erros durante desenvolvimento. Esse tipo de saída deve ser removido ou substituído por logging controlado em produção.
def exemplo_debug_form(request):
form = FormularioContato(request.POST or None)
if request.method == "POST":
if form.is_valid():
pass
else:
print("Erros do formulário:", form.errors)
print("Erros em JSON:", form.errors.as_json())
print("Erros gerais:", form.non_field_errors())
return render(request, "contato.html", {"form": form})
ModelForm: integração direta com modelos e validação antes de salvar
ModelForm é um formulário ligado a um modelo do Django, criado para reduzir código repetido ao editar ou criar registros. Campos, tipos e parte das validações são inferidos a partir do modelo. O mapeamento é definido na classe interna Meta, informando modelo e lista de campos. Widgets podem ajustar a apresentação sem alterar o modelo.
Mesmo com ModelForm, validações personalizadas continuam possíveis com clean_campo e clean(). Em uploads de imagem, é comum validar tamanho e garantir que o arquivo realmente é uma imagem. Uma validação adicional pode checar dimensões, o que reduz problemas de imagens minúsculas ou enormes. O exemplo a seguir apresenta um modelo de perfil e um ModelForm com validação de avatar e telefone.
from django.db import models
from django.contrib.auth.models import User
class PerfilUsuario(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(max_length=500, blank=True)
data_nascimento = models.DateField(null=True, blank=True)
avatar = models.ImageField(upload_to="avatares/", null=True, blank=True)
telefone = models.CharField(max_length=15, blank=True)
site = models.URLField(blank=True)
def __str__(self):
return f"Perfil de {self.user.username}"
from django import forms
from .models import PerfilUsuario
class PerfilUsuarioForm(forms.ModelForm):
class Meta:
model = PerfilUsuario
fields = ["bio", "data_nascimento", "avatar", "telefone", "site"]
widgets = {
"bio": forms.Textarea(attrs={"rows": 4}),
"data_nascimento": forms.DateInput(attrs={"type": "date"}),
"avatar": forms.FileInput(attrs={"accept": "image/*"}),
}
def clean_avatar(self):
avatar = self.cleaned_data.get("avatar")
if avatar:
if avatar.size > 2 * 1024 * 1024:
raise forms.ValidationError("A imagem deve ter no máximo 2MB.")
if not (avatar.content_type or "").startswith("image/"):
raise forms.ValidationError("Apenas arquivos de imagem são permitidos.")
return avatar
def clean_telefone(self):
telefone = self.cleaned_data.get("telefone")
if telefone:
t = telefone.replace(" ", "").replace("-", "")
if not (t.startswith("+") and t[1:].isdigit()):
raise forms.ValidationError("O telefone deve estar no formato: +1234567890.")
if len(t) < 10 or len(t) > 15:
raise forms.ValidationError("O telefone deve ter entre 10 e 15 dígitos.")
return t
return telefone
Formsets: formulários dinâmicos para múltiplos itens (exemplo de fatura)
Formset é um agrupador de múltiplas instâncias do mesmo formulário em uma única submissão. Esse recurso é útil para listas variáveis, como itens de uma fatura ou produtos em um pedido. O Django inclui campos de gerenciamento ocultos, como TOTAL_FORMS, que informam quantos formulários foram enviados. A validação ocorre por formulário e também no conjunto, respeitando limites como max_num.
O exemplo abaixo define um formulário de item e um formset associado. O cálculo do total soma quantidade vezes preço unitário para cada item válido. Formulários vazios podem existir, e por isso a verificação if form.cleaned_data evita processar entradas em branco. A consistência do índice dos campos é essencial para o Django reconstruir o formset corretamente.
from django import forms
from django.forms import formset_factory
class ItemFaturaForm(forms.Form):
descricao = forms.CharField(max_length=200)
quantidade = forms.IntegerField(min_value=1)
preco_unitario = forms.DecimalField(max_digits=10, decimal_places=2)
def clean_quantidade(self):
quantidade = self.cleaned_data.get("quantidade")
if quantidade and quantidade > 1000:
raise forms.ValidationError("A quantidade não pode exceder 1000.")
return quantidade
ItemFaturaFormSet = formset_factory(
ItemFaturaForm,
extra=1,
max_num=20,
validate_max=True
)
class FaturaForm(forms.Form):
numero = forms.CharField(max_length=50)
cliente = forms.CharField(max_length=100)
data_fatura = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}))
vencimento = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}))
observacoes = forms.CharField(required=False, widget=forms.Textarea(attrs={"rows": 3}))
Boas práticas de produção: consistência, segurança e prevenção de duplicidade
Em produção, formulários precisam ser previsíveis e resistentes a reenvios. O padrão Post/Redirect/Get reduz duplicidade ao impedir que o mesmo POST seja repetido no refresh. A validação deve sempre ocorrer no servidor, mesmo que exista validação no navegador, porque requisições podem ser forjadas. Uploads devem restringir tamanho e tipo, pois arquivos arbitrários ampliam a superfície de ataque.
Também é importante usar cleaned_data em vez de acessar valores diretamente em request.POST, porque o Django já normaliza dados e aplica validações. Erros devem ser exibidos de forma clara e ligada ao campo correto, evitando mensagens genéricas. Em situações de depuração, form.errors é a fonte central de verdade sobre falhas. Com esse conjunto, formulários se tornam uma camada confiável para cadastro, edição e envio de arquivos.
Conclusão
Django Forms fornecem uma base sólida para criar formulários com geração de HTML, validação e tratamento de erros de forma padronizada. A validação por campo e a validação geral do formulário organizam regras simples e complexas sem espalhar condicionais pela aplicação. Uploads seguros dependem de validações de extensão, tamanho e tipo, além do uso correto de request.FILES e multipart/form-data. A depuração se torna direta ao inspecionar form.errors e non_field_errors. Com ModelForms e formsets, o mesmo padrão se estende para integração com banco e cenários dinâmicos, mantendo consistência do início ao fim do fluxo.