O que é Django?
Django é um web framework para Python com a filosofia “batteries-included” (baterias inclusas). Enquanto a maioria dos frameworks exige que você escolha uma camada de banco de dados, um motor de templates, um painel administrativo e um sistema de autenticação, o Django entrega tudo isso em um único pacote coerente. Você descreve seus dados e suas páginas, e o framework cuida das partes tediosas e sensíveis à segurança.
Ele foi construído para a redação de um jornal em 2003 — uma equipe que precisava entregar funcionalidades rapidamente sem sucumbir aos prazos. Essa origem é evidente. O Django privilegia a convenção, a explicitude e a atitude de que “deve haver apenas uma maneira óbvia de fazer as coisas”, mantendo-se como uma das escolhas mais confiáveis para sites com grande volume de conteúdo e orientados a dados.
Se você já passou uma semana inteira conectando um ORM, uma ferramenta de migração e um sistema de autenticação, o Django é o framework que elimina essa semana de trabalho.
O padrão MTV
O Django chama sua arquitetura de MTV: Model, Template, View. É a mesma ideia do MVC, mas com nomes diferentes, e essa diferença costuma confundir as pessoas no início.
- Um model é uma classe Python que descreve uma tabela e suas linhas.
- Um template é um arquivo HTML com placeholders que renderiza os dados.
- Uma view é uma função ou classe que recebe uma requisição, solicita dados aos models e retorna uma resposta.
O controller, no sentido clássico do MVC, é o próprio Django. O dispatcher de URLs decide qual view será executada, e o framework faz a mediação entre as três camadas. Uma vez que esse diagrama esteja claro, todo o restante no Django torna-se um detalhe, e não mais um mistério.
Models e o ORM
Um model é uma classe que herda de models.Model. Cada atributo é um campo, e cada campo se torna uma coluna.
# blog/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
body = models.TextField()
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
CharField precisa de um max_length porque mapeia para um VARCHAR limitado. TextField é ilimitado. auto_now_add=True define um valor apenas na criação, enquanto auto_now=True atualiza a cada salvamento. null controla o banco de dados, e blank controla a validação do formulário; eles são separados propositalmente.
Os relacionamentos são explícitos:
ForeignKeyé muitos-para-um.ManyToManyFieldcria uma tabela de junção (join table).OneToOneFieldé uma chave estrangeira única.
O related_name define o acessor reverso, para que o user.posts.all() funcione do outro lado. O argumento on_delete é obrigatório porque o Django se recusa a adivinhar o que deve acontecer com os filhos quando um pai é deletado.
Migrations
Migrations são arquivos com controle de versão que descrevem as alterações no seu schema. Você escreve o model, e o Django escreve o SQL.
python manage.py makemigrations blog
python manage.py migrate
makemigrations inspeciona seus models e produz uma migration; migrate aplica as migrations pendentes ao banco de dados. Como os arquivos de migration ficam no seu repositório, todos os ambientes terminam com o mesmo schema, e as alterações podem ser revisadas como qualquer outro código. Nunca edite uma migration já aplicada — em vez disso, adicione uma nova.
Querysets e relacionamentos
O ORM retorna querysets, que são lazy (preguiçosos): eles não são executados até que você os itere, fatie ou avalie. Essa característica permite que você encadeie filtros e execute apenas a query final.
Post.objects.filter(status="PB").exclude(author=user).order_by("-published_at")[:10]
Métodos úteis de queryset incluem filter, exclude, get, first, exists, count, values, values_list, annotate e aggregate. As expressões F() permitem que você referencie uma coluna no banco de dados, e objetos Q() permitem que você construa condições OR.
from django.db.models import Count, Q
published = Post.objects.filter(
Q(status="PB") | Q(published_at__isnull=False)
).annotate(comment_count=Count("comments"))
É aqui que o ORM mostra seu valor: queries complexas permanecem em Python, são parametrizadas de forma segura e são compostíveis.
Evitando o problema N+1
O erro de performance mais comum no Django é a query N+1. Isso acontece quando você acessa um objeto relacionado dentro de um loop, disparando uma query extra para cada linha.
# 1 query for posts, then 1 query per post for the author
for post in Post.objects.all():
print(post.author.username)
O select_related faz joins de relações de valor único na query original, e o prefetch_related executa uma segunda query e une os resultados no Python para relações de múltiplos valores.
# 1 query for posts joined with author, 1 for the comments
posts = Post.objects.select_related("author").prefetch_related("comments")
Use select_related para foreign keys e campos one-to-one, e prefetch_related para relações many-to-many e relações reversas. Se uma página parecer lenta, conte as queries com o Django Debug Toolbar antes de otimizar qualquer outra coisa.
Views: funções e classes
Uma view é qualquer objeto chamável (callable) que recebe uma request e retorna uma response. Views baseadas em funções são o ponto de partida mais claro.
# blog/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_list(request):
posts = Post.objects.filter(status="PB").select_related("author")
return render(request, "blog/post_list.html", {"posts": posts})
def post_api(request, slug):
post = get_object_or_404(Post, slug=slug)
return JsonResponse({"title": post.title, "body": post.body})
Views baseadas em classes trocam um pouco de indireção por reuso. ListView, DetailView, CreateView, UpdateView e DeleteView implementam os padrões CRUD padrão, e mixins permitem que você componha comportamentos.
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(status="PB").select_related("author")
Use views baseadas em funções para lógicas pontuais e views baseadas em classes genéricas para páginas de listagem e detalhe padrão. Misturar ambas em uma base de código é normal e saudável.
URLs
O dispatcher de URL mapeia um caminho para uma view. Os padrões ficam em urls.py, e cada app pode manter os seus próprios.
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
]
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<slug:slug>/", views.post_detail, name="post_detail"),
]
Converters como <int:id> e <slug:slug> validam e tipam o segmento capturado. Sempre nomeie suas rotas e faça a resolução reversa com reverse() ou a tag {% url %} em vez de fixar os caminhos no código (hardcoding), para que os links sobrevivam a uma renomeação.
Templates
Templates são HTML com uma linguagem pequena e intencionalmente limitada. O autoescaping vem ativado por padrão, o que protege você contra XSS.
{% extends "base.html" %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>By {{ post.author.username }} on {{ post.published_at|date:"F j, Y" }}</p>
<div>{{ post.body|linebreaks }}</div>
{% endblock %}
{% extends %} oferece herança de templates, {% include %} compartilha fragmentos, e filtros como date, linebreaks e default formatam valores. Não existe Python arbitrário em templates; se um template precisa de lógica, essa lógica deve pertencer à view ou ao model.
Formulários e validação
O sistema de formulários do Django valida a entrada, renderiza o HTML e protege contra manipulações através de um token CSRF.
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "body", "status", "published_at"]
widgets = {"body": forms.Textarea(attrs={"rows": 12})}
# blog/views.py
from django.shortcuts import redirect, render
from .forms import PostForm
def post_create(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect(post)
else:
form = PostForm()
return render(request, "blog/post_form.html", {"form": form})
Um ModelForm deriva os campos e a validação a partir do model, garantindo que ambos nunca fiquem dessincronizados. Métodos de limpeza como clean_slug adicionam regras customizadas, e form.is_valid() coleta todos os erros antes de você acessar o banco de dados.
O admin
O admin é o recurso mais subestimado do Django. Registre um model e você terá uma interface pesquisável, filtrável e com controle de permissões para a equipe.
# blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "status", "published_at"]
list_filter = ["status", "created_at"]
search_fields = ["title", "body"]
prepopulated_fields = {"slug": ["title"]}
raw_id_fields = ["author"]
date_hierarchy = "published_at"
Isso é uma ferramenta interna completa em poucas linhas. Para sites de conteúdo, o admin é frequentemente o próprio produto. Ele também serve como uma barreira de segurança: permissões de staff, has_add_permission e has_change_permission controlam exatamente o que cada função pode fazer.
Configurações e apps
Um projeto Django é uma coleção de apps, onde cada um é um pacote independente com seus próprios models, views, templates e migrations. INSTALLED_APPS os lista, e settings.py armazena a configuração.
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog",
]
Mantenha as configurações fora do controle de versão para evitar a exposição de segredos. Leia-as de variáveis de ambiente, divida as configurações por ambiente conforme o projeto cresça e use django-environ ou pydantic-settings para processá-las. Os comandos django-admin startproject e startapp criam a estrutura de ambas as camadas, eliminando qualquer dúvida sobre a organização.
Padrões de segurança
Os padrões do Django foram escolhidos para que a opção mais segura seja também a mais fácil:
- Tokens CSRF protegem formulários que alteram estados; o middleware rejeita requisições que não os possuam.
- SQL injection é evitado porque os querysets parametrizam os valores.
- XSS é mitigado porque os templates escapam variáveis, a menos que você opte por desativar isso com
|safe. - Senhas são hasheadas com um algoritmo moderno e hashers configuráveis.
- Clickjacking é bloqueado pelo middleware
X-Frame-Options. - HTTPS é forçado em produção com
SECURE_SSL_REDIRECT,SESSION_COOKIE_SECUREeCSRF_COOKIE_SECURE.
Execute python manage.py check --deploy antes de fazer o deploy. Ele sinaliza as configurações que você esqueceu, e é muito mais barato do que uma revisão de segurança.
Construindo APIs com Django REST Framework
Para APIs JSON, o Django REST Framework (DRF) é o companheiro padrão. Um serializer descreve como um model se torna JSON e vice-versa, enquanto um viewset somado a um router gera os endpoints de CRUD.
# blog/serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
class Meta:
model = Post
fields = ["id", "title", "slug", "author", "status", "published_at"]
# blog/api.py
from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related("author")
serializer_class = PostSerializer
lookup_field = "slug"
# blog/urls.py
from rest_framework.routers import DefaultRouter
from .api import PostViewSet
router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
urlpatterns = router.urls
Serializers validam a entrada da mesma forma que os forms fazem, viewsets fornecem as operações de list, create, retrieve, update e delete nativamente, e o DRF pode gerar schemas OpenAPI para a sua API. O ORM do Django e as convenções do DRF são um caminho consolidado para APIs em produção.
Testes
O test runner do Django cria um banco de dados de teste novo e fornece um client que percorre todo o pipeline de requisição.
# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from .models import Post
class PostDetailTests(TestCase):
def setUp(self):
user = get_user_model().objects.create_user("ada", password="secret")
self.post = Post.objects.create(
title="Hello",
slug="hello",
body="First post",
author=user,
status=Post.Status.PUBLISHED,
)
def test_detail_page_renders(self):
response = self.client.get(reverse("blog:post_detail", args=["hello"]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Hello")
def test_missing_post_returns_404(self):
response = self.client.get(reverse("blog:post_detail", args=["nope"]))
self.assertEqual(response.status_code, 404)
TestCase envolve cada teste em uma transação e realiza o rollback, garantindo que os testes permaneçam isolados e rápidos. Use django.test.Client para views, o ORM diretamente para models, e pytest-django se preferir pytest fixtures. Foque em testar o comportamento e as permissões, não os detalhes de implementação.
Melhores práticas
- Mantenha os models focados em dados e métodos de domínio simples; coloque a orquestração em services ou views.
- Sempre execute
select_relatedeprefetch_relatedonde um template utilize relações. - Use
ModelForme serializers do DRF para que a validação fique concentrada em um único lugar. - Nomeie cada URL e utilize o reverse; nunca escreva caminhos (paths) fixos em templates ou no código.
- Adicione índices no banco de dados para colunas que você utiliza em filtros e ordenações.
- Mantenha segredos em variáveis de ambiente e execute
check --deployantes do release. - Escreva migrations como artefatos de code review e nunca edite uma migration que já foi aplicada.
- Teste o pipeline de requisições com
Cliente a lógica de domínio diretamente.
Erros comuns
- Iterar sobre um queryset e acessar relações dentro do loop, causando queries N+1.
- Usar
null=Trueem um campo de string em vez de uma string vazia, e depois ter problemas com verificações deNone. - Colocar lógica de negócio em templates porque a view parecia muito poluída.
- Chamar
save()dentro de um loop em vez debulk_createoubulk_update. - Esquecer o
on_deleteou escolherCASCADEsem pensar na perda de dados. - Deixar
DEBUG = Truee umSECRET_KEYhardcoded em produção. - Tratar o admin como um frontend para o usuário final em vez de uma ferramenta para a equipe.
- Fazer queries com
len(queryset)em vez de.count(), carregando cada linha na memória.
Próximos passos
O Django oferece o caminho completo e convencional para criar um app web em Python pronto para produção. Se você prefere um núcleo mais leve e mais opções de escolha, leia o guia de Flask a seguir. Se estiver construindo uma API JSON assíncrona moderna e quiser que os tipos cuidem da validação, vá para o FastAPI. Independentemente da sua escolha, invista no banco de dados: indexação, transações e planejamento de queries importam mais do que o framework, e o roadmap de backend aprofunda esses conceitos. O guia de REST APIs aborda as regras de design que tornam uma API agradável de consumir.