Por que Django neste projeto?


Como comentei no primeiro post, o objetivo era a criação de um blog técnico para reduzir a fricção entre escrita e leitura. Para isso, organização, controle de backups, deploy e funcionalidades customizadas eram prioridade.

No post Arquitetura e decisões do LearningSea (visão geral), eu cito brevemente o porque principal da escolha do Django.

O framework foi escolhido principalmente por fornecer tudo que uma aplicação orientada a conteúdo precisa, sem exigir a construção desses componentes do zero;

Usar um framework me permitiu não precisar reinventar partes previsíveis de uma aplicação web. Na prática, isso significava que eu não precisava tomar uma série de decisões básicas:

  • modelar e acessar dados → o ORM já resolve isso
  • gerenciar conteúdo → o admin já me dá um painel completo
  • renderizar páginas → o sistema de templates já está integrado
  • lidar com autenticação → já vem pronto

Django me deu a estrutura – mas transformar isso em um blog funcional ainda exige decisões reais sobre como essas peças devem se comportar — e é aí que está o verdadeiro desafio.

Além disso, o que acontece quando alguém faz uma request? É exatamente esse fluxo que vamos percorrer a seguir.

Fluxo da requisição


Quando alguém faz uma request para o blog, o fluxo segue, de forma simplificada, este caminho:

Client envia HTTP Request → Web Server → Application Server → Django cria HttpRequest → Middleware stack (request phase) → URL resolver → View (orquestra a lógica) → ORM → SQL → Banco de Dados → Model instance → Template rendering → HTML → Middleware stack (response phase) → Django retorna HttpResponse → Application Server → Web Server → Client (browser renderiza HTML)

Vamos aprofundar em algumas dessas partes para um maior entendimento do pipeline acima.

Middleware


Middlewares existem para resolver preocupações globais do sistema em um único lugar, garantindo que toda requisição já chegue à aplicação com uma série de validações, mais segura e com o contexto necessário.

Podemos pensar nos middlewares como uma cadeia de interceptadores entre o momento em que a request chega e o momento em que a response sai.

Cada middleware pode ler, modificar ou bloquear uma request. Na volta, eles também podem modificar a response.

Sem middleware, precisaríamos implementar essas validações em cada view individualmente, o que rapidamente se torna repetitivo, difícil de manter e propenso a erros.

Na prática, isso significa que quando a request chega na view, ela está mais "limpa" — ela já foi filtrada, validada e enriquecida com informações importantes, como o usuário autenticado, por exemplo.

Exemplo:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware', # Endurece a segurança no nível HTTP e adiciona headers de segurança (HSTS, etc.)
    'django.contrib.auth.middleware.AuthenticationMiddleware', # Identifica o usuário, assim podemos acessar request.user
    'axes.middleware.AxesMiddleware',  # Monitora tentativas de login, bloqueia IP após falhas repetidas. Precisa ser o último
]

URL resolver


Depois que a request passa pela cadeia de middlewares, o Django precisa decidir qual parte da aplicação deve responder aquela URL.

É nesse ponto que entra o URL resolver.

De forma simples, o urls.py funciona como uma tabela de roteamento: ele recebe o caminho da requisição e tenta encontrar um padrão que corresponda àquela URL. Essa verificação acontece de cima para baixo, e o primeiro padrão que casa com a URL é o escolhido.

No contexto do blog, isso significa mapear caminhos como:

  • /about/ → página estática
  • /post/<slug:slug>/ → página de um post específico

Um detalhe importante aqui é que a ordem das rotas importa. Por exemplo, uma rota genérica como <slug:slug>/ pode capturar praticamente qualquer URL, então se ela vier antes de rotas mais específicas como about/, ela pode interceptar requisições que não deveria.

Isso mostra que o roteamento não é apenas uma configuração declarativa — ele também envolve decisões sobre como o conteúdo deve ser acessado.

Na prática, isso se traduz em algo como:

urlpatterns = [
    path('', views.PostListView.as_view(), name='post_list'),
    path('about/', views.AboutView.as_view(), name='about'),
    path('post/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),

Views


Uma vez que a URL é resolvida, o Django sabe exatamente qual view deve ser chamada para responder aquela request.

Uma view é responsável por transformar uma request em uma response.
Se pensarmos em uma analogia, poderíamos descrever que quando uma requisição chega a uma aplicação web, ela é como alguém entrando em um centro de triagem com um problema específico. A URL funciona como a descrição desse problema, e o sistema de rotas decide para qual “especialista” aquele caso deve ser encaminhado. Esse especialista é a view: ela recebe o pedido, analisa os dados disponíveis, consulta informações se necessário e toma uma decisão. No final, ela devolve um resultado — que pode ser uma página, dados em formato JSON ou até um redirecionamento. Em essência, a view é o ponto onde a aplicação deixa de apenas encaminhar e passa a realmente pensar e agir sobre a requisição.

No contexto do blog, isso significa uma série de decisões:

  • qual idioma deve ser exibido
  • quais posts o usuário pode ver
  • se há uma busca ativa
  • como esses dados devem ser organizados

Como essas decisões aparecem em praticamente todas as views — como identificar o idioma, aplicar regras de acesso e organizar os dados, faz sentido estruturar isso de forma reutilizável.
Para isso, utilizei class-based views (CBVs) e mixins.

Class-based views

No Django, existem duas formas principais de escrever views: function-based views (FBV) e class-based views (CBV). Enquanto as FBVs oferecem controle total sobre o fluxo, as CBVs encapsulam padrões comuns de comportamento.

No meu caso, utilizei class-based views como ListView e DetailView, que já implementam boa parte da lógica necessária para listar objetos e exibir detalhes. Isso reduz a quantidade de código repetitivo e permite focar apenas nas partes que realmente precisam de customização, como a definição do queryset.

Na prática (exemplo simplificado):

class PostListView(ListView):
    model = Post  # define qual modelo será usado na listagem
    template_name = 'blog/post_list.html'  # template que será renderizado
    context_object_name = 'posts'  # nome da variável no template

    def get_queryset(self):
        return Post.objects.order_by('-created_at')  # busca e ordena os posts

Mixin

Mixins são uma forma de adicionar comportamento reutilizável a uma classe por meio de herança. Diferente de uma classe completa, um mixin não existe sozinho — ele representa apenas um “pedaço” de lógica que pode ser combinado com outras classes.

No blog, utilizo dois mixins principais:

  • LanguageMixin: responsável por determinar o idioma da requisição com base na URL (/en/ ou padrão em português)
  • StaffOrPublishedMixin: responsável por definir quais dados podem ser acessados, permitindo que usuários comuns vejam apenas posts publicados, enquanto usuários com permissão de staff conseguem visualizar todos

Esses comportamentos aparecem em praticamente todas as views. Ao extraí-los para mixins, evito repetição de código e centralizo regras importantes em um único lugar.

Na prática (exemplo):

class StaffOrPublishedMixin:
    def get_base_queryset(self):
        # se for staff, retorna todos os posts
        if self.request.user.is_staff:
            return Post.objects.all()

        # caso contrário, retorna apenas posts publicados
        return Post.objects.filter(status='published')

Esse segundo caso introduz um conceito importante: o base queryset.

O base queryset representa o conjunto inicial de dados permitido antes de qualquer outro filtro ser aplicado. No caso do blog, ele define se a consulta deve retornar todos os posts ou apenas os publicados, dependendo do usuário. A partir desse ponto, cada view pode especializar o comportamento — ordenar, filtrar ou buscar, sem precisar se preocupar novamente com regras de acesso.

QuerySet e busca

A partir do base queryset, as views constroem consultas mais específicas. Isso é feito utilizando o ORM do Django, que permite escrever queries em Python que serão traduzidas para SQL.

Um detalhe importante é que o queryset não executa imediatamente. Ele é construído de forma incremental — com filtros, ordenações e condições, mas só é avaliado quando os dados são realmente necessários, normalmente no momento em que o template itera sobre os resultados. Esse comportamento é conhecido como lazy evaluation.

Isso permite compor consultas de forma eficiente. Por exemplo, na listagem de posts, o fluxo segue aproximadamente assim:

  • começa com o base queryset (controle de acesso)
  • aplica ordenação por data
  • verifica se existe um termo de busca
  • caso exista, adiciona filtros utilizando Q objects, permitindo combinar múltiplos campos com condições OR

Essa abordagem resulta em uma única query final ao banco de dados, mesmo que ela tenha sido construída em várias etapas.

A busca em si é baseada em icontains, o que permite encontrar ocorrências parciais de texto de forma simples. É uma solução suficiente para o contexto do blog, embora não seja a mais escalável para grandes volumes de dados.

Na prática (exemplo simplificado):

from django.db.models import Q

queryset = Post.objects.all()  # base inicial da query

# aplica filtro de busca em múltiplos campos
queryset = queryset.filter(
    Q(title__icontains=query) |      # busca no título
    Q(excerpt__icontains=query) |    # busca no resumo
    Q(content__icontains=query)      # busca no conteúdo
)

Organização das views

Com esses elementos, as views seguem uma estrutura relativamente consistente.

A PostListView é responsável por listar os posts, aplicando ordenação, busca e contexto adicional para o template. Ela funciona como uma pipeline: recebe o estado da requisição, transforma isso em uma consulta ao banco e retorna os dados prontos para renderização.

A PostDetailView trabalha com um único objeto, resolvido a partir do slug da URL. Além disso, ela decide qual versão do conteúdo deve ser exibida com base no idioma, incluindo um fallback para português quando não há tradução disponível.

As views de filtro por categoria e tag reutilizam a mesma lógica de listagem, alterando apenas o critério de filtragem. Isso mostra como a composição permite variar o comportamento sem duplicar estrutura.

Por fim, a AboutView representa o caso mais simples: uma página estática renderizada com base no idioma, sem necessidade de acesso ao banco de dados.

Essas decisões mostram que a view não apenas retorna dados — ela define como o sistema se comporta diante de cada requisição.

Models


Até aqui, a aplicação já passou pelo roteamento de URLs, que direciona a requisição para uma view. A view constrói um queryset e o passa para o template, que consome esses dados e força a execução da query. É nesse ponto que o papel dos models começa a ficar mais claro.

Os models no Django são classes Python que representam tabelas no banco de dados. Cada instância de um model corresponde a uma linha, e cada atributo representa uma coluna.

Models são uma das abstrações centrais do Django, pois permitem trabalhar com o banco de dados utilizando apenas Python. Dessa forma, a preocupação passa a ser modelar os dados, não escrever a sintaxe específica do SGBD, o que também facilita a troca de banco de dados caso necessário.

Os models do blog são estruturados em três entidades principais: Post, Category e Tag.

Nos models, o slug é gerado automaticamente a partir do nome ou título no momento do salvamento. Isso evita inconsistências e elimina a necessidade de definir URLs manualmente.

Além disso, o model suporta conteúdo em dois idiomas (português e inglês), com campos separados. Essa escolha simplifica a escrita e mantém a lógica de fallback controlada nas views.

Category e Tag

Os models Category e Tag são simples, compostos basicamente por name e slug. O método save() é sobrescrito apenas para gerar o slug automaticamente a partir do nome.

Esse relacionamento fica mais claro no model Post.

Post

No model Post, Category é utilizado como uma ForeignKey, o que significa que cada post possui apenas uma categoria.
Tag é utilizado como um ManyToMany, permitindo que um post esteja associado a múltiplas tags. Esse tipo de relação utiliza uma tabela intermediária automaticamente gerada pelo Django, responsável por conectar posts e tags.

Na prática:

class Post(models.Model):
...
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='posts'
    )
    tags = models.ManyToManyField(
        Tag,
        blank=True,
        related_name='posts'
    )
...

Tipo de editor

O model post também define o tipo de editor utilizado para o conteúdo:

class Post(models.Model):
    class Status(models.TextChoices):
        DRAFT = 'draft', 'Draft'
        PUBLISHED = 'published', 'Published'

    class EditorType(models.TextChoices):
        HTML = 'html', 'HTML (Summernote)'
        MARKDOWN = 'markdown', 'Markdown'
...

O porque dessa escolha foi explicada em posts anteriores:

Devido a projetos de estudos anteriores em Django, eu conhecia o editor de texto Summernote.
Para maior praticidade e versatilidade, também optei por incluir suporte à escrita em Markdown. Para desenvolvedores, é uma forma mais rápida e precisa de estruturar conteúdo em comparação a um editor WYSIWYG como o Summernote, além de permitir escrita fora da aplicação, em qualquer editor de texto.

Sobrescrição do método save()

Aqui entra um dos detalhes mais importantes do model Post. Esse comportamento é implementado diretamente no método save().
É nele que acontece o processamento do conteúdo antes de ser persistido no banco. Quando o editor utilizado é Markdown, o texto é convertido para HTML utilizando a biblioteca markdown. Em seguida, esse HTML passa por um processo de sanitização.

Essa etapa é essencial. Ela garante que o conteúdo armazenado seja seguro, removendo possíveis elementos maliciosos. Como essa validação acontece no momento da escrita, o sistema pode renderizar o HTML diretamente no template utilizando |safe, sem comprometer a segurança da aplicação.

Na prática:

class Post(models.Model):
...
    if self.editor_type == self.EditorType.MARKDOWN and self.content_markdown:
        self.content = markdown.markdown(
            self.content_markdown,
            extensions=['fenced_code', 'tables', 'toc', 'nl2br']
        )
    self.content = sanitize_content(self.content)
...

No fim, os models não apenas definem a estrutura dos dados, mas também controlam como esses dados entram e são validados no sistema — garantindo consistência, segurança e previsibilidade.

Templates


Até aqui, os dados já foram tratados pelos middlewares, encaminhados pelo roteamento de URL, processados pela view e estão com o queryset pronto, contendo os dados que serão consumidos pelo template. Agora, a questão é como esses dados serão apresentados para o client (browser).

Os templates resolvem exatamente esse problema — escrevemos HTML normalmente, com a diferença de que podemos utilizar uma linguagem própria do Django para acessar variáveis e aplicar lógica simples dentro do template. Isso é possível porque o Django processa o template antes de enviar o HTML para o client.

Outro ponto extremamente útil é a possibilidade de herança de templates, como no caso de um base.html, que contém a estrutura comum do site. A partir dele, outros templates podem sobrescrever blocos específicos utilizando {% extends %}. Também é possível reutilizar partes menores com {% include %}, como header.html e footer.html.

Exemplo de como a linguagem de template se parece dentro de um HTML:

{% block content %}
  {% for post in page_obj %}
  <article class="post-card">
    {% if site_setup.show_post_miniature_image %}
      {% if post.cover %}
        <div class="post-image">
      <a href="{{ post.get_absolute_url }}"><img src="{{ post.cover.url }}" alt="Image for the {{ post.title }}"></a>
        </div>
      {% endif %}
    {% endif %}

    <div class="post-content">
      <a href="{{ post.get_absolute_url }}"><h2 class="post-title">{{ post.title }}</h2></a>
      <p class="post-excerpt">
        {{ post.excerpt }}
      </p>
      <a class="post-link" href="{{ post.get_absolute_url }}">Read -></a>
    </div>
  </article>
  {% empty %}
    <h2>No posts yet.</h2>
  {% endfor %}
{% endblock %}

Nesse exemplo, é possível ver como o sistema de templates permite a renderização de dados dinâmicos diretamente no HTML.

Todo o processamento dos templates acontece do lado do servidor — o HTML só é enviado para o browser depois de pronto. Isso é conhecido como SSR (Server Side Rendering).

Uma alternativa é o CSR (Client Side Rendering), onde o navegador faz requisições adicionais via JavaScript para buscar dados, geralmente através de uma API. Nesse modelo, o backend retorna apenas dados (geralmente em JSON), e o frontend fica responsável por montar a interface.

Comparando os dois, no SSR o servidor processa os dados e monta o HTML, enquanto no CSR o servidor entrega apenas os dados, e o HTML é construído no navegador.

No contexto do blog, o SSR se encaixa naturalmente. O conteúdo é majoritariamente estático, não exige interatividade complexa e se beneficia de já ser entregue pronto ao usuário.
Isso reduz a complexidade da aplicação, elimina a necessidade de uma camada extra de frontend e mantém o foco no que realmente importa: escrever e servir conteúdo.

Admin Panel


O Admin Panel fornece uma interface pronta para gerenciar o conteúdo do blog — essencialmente um CRUD completo para posts, categorias e tags, além da gestão de usuários e permissões.

A principal vantagem é que tudo isso já vem integrado aos models, sem necessidade de implementação manual.

Sem utilizar o Admin Panel, seria necessário construir essa camada do zero: templates para formulários, lógica para criação, atualização e remoção de dados, validação de campos, controle de autenticação e proteção contra CSRF, além da própria interface de uso.

Na prática, isso significaria desenvolver um pequeno CMS (Content Management System) apenas para gerenciar o conteúdo da aplicação.

Exemplo da configuração do admin.py:

# Registra o model Post no Django Admin
@admin.register(Post)
class PostAdmin(SummernoteModelAdmin):

    # Define quais campos usam o editor rich text (Summernote)
    summernote_fields = ['content', 'content_en']
    # quando o tipo de editor for HTML, esses campos são usados

    # Campos exibidos na listagem de posts
    list_display = ['title', 'editor_type', 'status', 'created_at']
    # inclui o tipo de editor para identificar rapidamente como o conteúdo foi escrito

    # Gera automaticamente o slug a partir do título
    prepopulated_fields = {'slug': ('title',)}

    # Permite buscar posts pelo título
    search_fields = ['title']

    # Filtros laterais no admin
    list_filter = ['status', 'editor_type', 'created_at']
    # facilita filtrar por posts em Markdown ou HTML

    # Organização dos campos no formulário de edição
    fieldsets = [
        ('Conteúdo principal', {
            'fields': [
                'title',
                'slug',
                'editor_type',       # define se o conteúdo será HTML ou Markdown
                'content_markdown',  # usado quando editor_type = Markdown
                'content',           # HTML final (renderizado ou escrito direto)
                'status',
            ]
        }),
        ('Tradução (Inglês)', {
            'fields': [
                'content_markdown_en',
                'content_en',
            ],
            'classes': ['collapse'],  # começa escondido, expande ao clicar
        }),
    ]

Esse exemplo mostra como o Django Admin pode ser adaptado ao fluxo real de escrita do projeto. O campo editor_type permite alternar entre HTML e Markdown, enquanto o Summernote facilita a edição quando o conteúdo é escrito diretamente em HTML. No caso de Markdown, o texto é armazenado separadamente e convertido antes de ser exibido. Além disso, os fieldsets organizam os campos e mantêm a versão em inglês colapsada por padrão, reduzindo a complexidade visual do formulário.

Conclusão


O Django utiliza o padrão MVT (Model-View-Template), que é conceitualmente muito próximo do MVC (Model-View-Controller). A principal diferença está na nomenclatura: no Django, a “view” assume o papel do controller, enquanto o template é responsável pela camada de apresentação.

Essa arquitetura define um caminho específico a ser seguido na construção da aplicação. Isso torna o desenvolvimento mais rápido, previsível e fácil de manter.

Depois de entender cada uma dessas partes, o fluxo da aplicação pode ser resumido de forma simples:

Request → Middleware → URL → View → DB → Template → Response

A request chega no servidor, passa pelas etapas de processamento e retorna como uma response pronta para o navegador.