Até agora, discutimos como construir páginas web simples usando HTML, CSS e JavaScript. Também começamos a usar FastAPI para criar aplicações web, e aprendemos a utilizar bancos de dados para armazenar informações em nossos sites.
Hoje, discutiremos conceitos de arquitetura de software e HTMX com alguns exemplos práticos.
CRUD é um acrônimo que representa as operações básicas de um banco de dados (Create, Read, Update, Delete). Em uma API, como o FastAPI, essas operações costumam estar embutidas dentro de outras funções da própria API, mas sempre estão presentes em todo banco de dados relacional. Em HTTP, as operações CRUD são traduzidas em Create = POST, Read = GET, Update = PUT e Delete = DELETE.
REST (Representational State Transfer) é um estilo de arquitetura de software para APIs que facilita a comunicação entre sistemas web. O conceito fundamental do REST, que faz com que ele se destaque, é a Interface Uniforme, que possui quatro características principais:
Para explicar esses conceitos, considere o trecho de HTML abaixo:
<body>
<section>
<p>
Name: Fulano
</p>
<p>
Email: fulano@usp.br
</p>
<p>
<a href="/contatos/37/editar">Editar Contato</a>
<a href="/contatos/37/email">Acessar Email</a>
<a href="/contatos/37/arquivo">Arquivar</a>
</p>
</section>
</body>
Suponha que este HTML é retornado pelo servidor ao acessar www.exemplo.com/contatos/37.
Vamos passar por cada característica e analisar o código acima:
Identificação de recursos: O primeiro aspecto do REST é a idéia de que recursos (nesse caso, o contato) podem ser encontrados na URL (Universal Resource Locator). Note também que o HTML, retornado pelo servidor ao acessar a URL, contém links em tags <a> que indicam as operações que são aplicáveis ao nosso recurso (contato 37) e elas seguem o padrão convencional de hierarquia das URLs.Manipulação de recursos por representação: Isso apenas significa que é possível alterar nosso recurso por meio de representações (como páginas HTML) ao invés de precisar fazer consultas SQL, por exemplo.Mensagens auto-descritivas: Mesmo com o cliente (browser) não tendo nenhuma informação sobre o funcionamento da API ou o banco de dados, ele tem todas as informações sobre quais ações estão disponíveis para esse recurso e como renderizá-las.HATEOAS: Este último conceito se encaixa perfeitamente com o anterior: os clientes transitam entre os estados da aplicação interagindo com URLs encontradas no próprio hipertexto (por meio de formulários e links). Assim, no exemplo de HTML acima, a capacidade de editar, enviar por e-mail e arquivar o contato está codificada como âncoras no HTML. Se uma dessas ações não estivesse disponível, ou se uma nova se tornasse disponível, ela apareceria em um novo trecho de HTML após a atualização da página. Isso contrasta com uma abordagem de cliente robusto, onde, por exemplo, um armazenamento local pode ser sincronizado de forma assíncrona com um servidor back-end e, portanto, o HTML não atua como o mecanismo do estado do aplicativo, mas sim como uma linguagem de descrição de interface do usuário (um tanto instável).Atualmente, a arquitetura REST é muitas vezes referenciada erroneamente. É comum a associação de REST com APIs de JSON, duas coisas que não são iguais. Até mesmo pois JSON não é totalmente auto-descritivo, por exemplo.
Leia mais sobre essa confusão nesse artigo da própria ORG HTMX.
Anteriormente, se quiséssemos um site com múltiplas páginas, faríamos isso usando rotas diferentes em nossa aplicação. Agora, temos a capacidade de carregar apenas uma única página e então usar JavaScript para manipular a DOM. Uma grande vantagem disso é que só precisamos modificar a parte da página que está realmente mudando. Por exemplo, se temos uma Barra de Navegação que não muda de uma página para outra, não gostaríamos de ter que renderizar novamente essa navbar toda vez que mudamos para uma nova página.
Essa é a diferença de uma SPA (Single Page Application) para uma MPA (Multi-Page Application): MPAs recarregam toda a página no redirecionamento, enquanto SPAs apenas atualizam o conteúdo da página atual. Ambas as estratégias possuem suas vantagens e desvantagens.
Exemplos de MPAs são a Wikipedia, Amazon, ...
Exemplos de SPAs são o Gmail, Instagram, Netflix, ...
As SPAs costumam ter carregamento mais rápido, são mais simples de desenvolver (dado que o código de front-end é reutilizado múltiplas vezes) e facilitam a implementação de interações com o usuário como animações e atualizações em tempo real da página. Por outro lado, utilizam mais memória e possuem um carregamento inicial mais longo.
Já as MPAs não possuem uma dependência em JavaScript como as SPAs, também são mais adequadas para aplicações grandes, com conteúdo diversificado e/ou muito processamento de dados, mas são mais lentas e exigem mais manutenção de código do que as SPAs.
| MPA | SPA |
|---|---|
![]() |
![]() |
Requests AJAX (Asynchronous JavaScript and XML) permitem que páginas web se comuniquem com o servidor de forma assíncrona, ou seja, sem recarregar a página inteira. Isso melhora a experiência do usuário, pois permite que apenas partes específicas da página sejam atualizadas. AJAX se refere a uma técnica e não uma tecnologia ou ferramenta específica, por isso existem várias formas de realizar esse tipo de request e atualizar o conteúdo da página.
HTMX é uma biblioteca de JavaScript que permite acessar recursos modernos dos navegadores diretamente do HTML, em vez de escrever seu próprio JavaScript.
Para entender o HTMX, vamos dar uma olhada em uma tag âncora:
<a href="/blog">Blog</a>
Esta tag âncora diz ao navegador:
"Quando um usuário clicar neste link, faça uma requisição HTTP GET para '/blog' e carregue o conteúdo da resposta na janela do navegador".
Com isso em mente, considere o seguinte trecho de HTML com HTMX:
<button hx-post="/clicked"
hx-trigger="click"
hx-target="#parent-div"
hx-swap="outerHTML">
Clique Aqui!
</button>
Isso diz ao HTMX:
"Quando um usuário clicar neste botão, faça uma requisição HTTP POST para /clicked e use o conteúdo da resposta para substituir o outerHTML do elemento com o id #parent-div na DOM"
O HTMX estende e generaliza a ideia central do HTML como hipertexto, abrindo muitas possibilidades diretamente na linguagem:
GET e POST, pode ser usadoObserve que quando você está usando HTMX, no lado do servidor você normalmente responde com HTML, não com JSON.
Para utilizar o HTMX, devemos incluir a seguinte linha no head do nosso HTML:
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.js" integrity="sha384-ezjq8118wdwdRMj+nX4bevEi+cDLTbhLAeFF688VK8tPDGeLUe0WoY2MZtSla72F" crossorigin="anonymous"></script>
Vamos começar com um exemplo simples, entendendo como usar HTMX para atualizar o conteúdo da tela de forma dinâmica. No exemplo abaixo temos um template do FastAPI mostrando uma página inicial /home que possui botões de redirecionamento:
<!-- Arquivo index.html -->
<DOCTYPE html>
<html lang="pt-br">
<head>
<title>Meu Site</title>
<style>...</style>
</head>
<body>
<h1>Meu Site</h1>
<p>Bem vindo ao meu site!</p>
<nav>
<a href="/home/pagina1"><button>Pagina 1</button></a>
<a href="/home/pagina2"><button>Pagina 2</button></a>
</nav>
<hr>
{% block main %}
<h2>Pagina 1</h2>
<p>Conteúdo da página 1</p>
{% endblock main %}
</body>
</html>
<!-- Arquivo Pagina1.html -->
{% extends "index.html" %}
{% block main %}
<h2>Pagina 1</h2>
<p>Conteúdo da página 1</p>
{% endblock main %}
<!-- Arquivo Pagina2.html -->
{% extends "index.html" %}
{% block main %}
<h2>Pagina 2</h2>
<p>Conteúdo da página 2</p>
{% endblock main %}
Por mais que pareça, o código acima não é uma SPA pois o FastAPI concatena os arquivos html quando realiza o request /home/pagina1, por exemplo. Logo, o HTML retornado pelo request de uma dessas páginas será o HTML completo da página.
Assim, pensávamos na navegação entre páginas dentro da lógica de uma MPA (Multi Page Application), em adicionar anchor tags com links que recarregam a tela e nos levam para uma outra página a partir de uma resposta do servidor contendo um arquivo HTML completo. Com HTMX, entretanto, podemos ser mais eficientes e trocar somente as partes que nos interessam.
Vamos adaptar o exemplo acima para que ele utilize HTMX e se comporte mais como uma SPA.
<!-- Arquivo index.html -->
<DOCTYPE html>
<html lang="pt-br">
<head>
<title>Meu Site</title>
<style>...</style>
<script src="HTMX"></script> <!-- Import do HTMX -->
</head>
<body>
<h1>Meu Site</h1>
<p>Bem vindo ao meu site!</p>
<nav>
<button hx-get="/home/pagina1"
hx-target="main">
Página 1
</button>
<button hx-get="/home/pagina2"
hx-target="main">
Página 2
</button>
</nav>
<hr>
<main>
<h2>Pagina 1</h2>
<p>Conteúdo da página 1</p>
</main>
</body>
</html>
<!-- Arquivo Pagina1.html -->
<h2>Pagina 1</h2>
<p>Conteúdo da página 1</p>
<!-- Arquivo Pagina2.html -->
<h2>Pagina 2</h2>
<p>Conteúdo da página 2</p>
Perceba que mudamos algumas coisas: importamos o HTMX dentro da head e encapsulamos o conteúdo da página dentro de uma tag 'main', ao invés de utilizar os blocos da linguagem de template. O atributo hx-target utiliza esse encapsulamento para trocar o conteúdo que está no local indicado, no caso, a tag 'main'. Além disso, não especificamos o hx-swap que nos diz como a resposta do servidor vai substituir nosso alvo, isso porque o hx-swap padrão é o 'innerHTML'.
Após essas alterações, a resposta para o request feito por hx-get="/home/pagina2" será apenas
<h2>Página 2</h2>
<p>Conteúdo da página 2</p>
Essa resposta subistituirá o conteúdo 'innerHTML' da tag 'main' e a página 2 passará a ser renderizada.
O mesmo vale para o botão para fazer o request da página 1: Quando o botão for pressionado, apenas o html de 'Pagina1.html' será retornado.
Agora que temos todo o código modularizado, podemos até mesmo omitir o conteúdo da página 1 no nosso 'index.html'. No entanto, ao recarregar a página inicial, o conteúdo da página 1 não estará mais renderizado pois o retiramos do HTML base. Uma forma possível de resolver esse problema é solicitar a página 1 no momento que a página inicial for carregada, e podemos fazer isso com o atributo hx-trigger="load" .
<!-- Arquivo index.html -->
<DOCTYPE html>
<html lang="pt-br">
<head>
<title>Meu Site</title>
<style>...</style>
<script src="HTMX"></script> <!-- Import do HTMX -->
</head>
<body>
<h1>Meu Site</h1>
<p>Bem vindo ao meu site!</p>
<nav>
<button hx-get="/home/pagina1"
hx-target="main">
Página 1
</button>
<button hx-get="/home/pagina2"
hx-target="main">
Página 2
</button>
</nav>
<hr>
<main hx-get="/home/pagina1"
hx-target="main"
hx-trigger="load">
</main>
</body>
</html>
Agora, ao acessar /home, a página automaticamente fará o request de /home/pagina1 e renderizará o conteúdo da página 1 sem que tenhamos que fazer o request pelo botão.
Continuando a analisar nosso exemplo anterior, temos que agora nossa aplicação funciona como uma SPA.
Note que a URL da nossa aplicação não muda, ou seja, ela permanece como /home independente da página que está sendo visualizada. Embora esse seja um comportamento comum de um aplicação SPA, talvez seja desejável utilizar a URL para visualizar a página 2 diretamente, sem precisar passar pela página 1.
Para resolver esse problema, precisamos primeiramente ajustar nossa aplicação para que as visualizações das páginas estejam vinculadas a URL. O HTMX possui o atributo hx-push-url que atualiza a URL atual com a URL do request feito. Assim, podemos adicionar esse atributo aos nossos requests no 'index.html'.
<!-- Arquivo index.html -->
<DOCTYPE html>
<html lang="pt-br">
<head>
<title>Meu Site</title>
<style>...</style>
<script src="HTMX"></script> <!-- Import do HTMX -->
</head>
<body>
<h1>Meu Site</h1>
<p>Bem vindo ao meu site!</p>
<nav>
<button hx-get="/home/pagina1"
hx-target="main"
hx-push-url="true">
Página 1
</button>
<button hx-get="/home/pagina2"
hx-target="main"
hx-push-url="true">
Página 2
</button>
</nav>
<hr>
<main hx-get="/home/pagina1"
hx-target="main"
hx-trigger="load"
hx-push-url="true">
</main>
</body>
</html>
Por padrão, o hx-push-url possui o valor falso, mas podemos atualizá-lo atribuindo o valor 'true'.
Agora, toda vez que atualizarmos a página que está sendo visualizada, a URL também será atualizada para /home/paginaX, com X o número da página.
Mesmo com essa modificação, ainda não é póssível acessar a página 2 diretamente pela URL. Note que se acessarmos /home/pagina1 apenas o conteúdo da página 1 será renderizado, sem o cabeçalho do site, e o mesmo vale para /home/pagina2.
Para resolver esse problema, teremos que atualizar nossa main.py. Considere o código abaixo como sendo a main.py atual.
# Arquivo main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory=["Templates", "Templates/Partials"])
@app.get("/home",response_class=HTMLResponse)
async def root(request: Request):
return templates.TemplateResponse(request, "index.html")
@app.get("/home/pagina1", response_class=HTMLResponse)
async def pag1(request: Request):
return templates.TemplateResponse(request, "Pagina1.html")
@app.get("/home/pagina2", response_class=HTMLResponse)
async def pag2(request: Request):
return templates.TemplateResponse(request, "Pagina2.html")
É possível realizar uma verificação sobre o tipo de request que está sendo feito, e isso nos possibilita separar requests HTMX dos demais. Dessa forma, quando detectarmos que o request para o conteúdo das páginas não é HTMX (foi um request feito diretamente pela URL), podemos pedir para que seja renderizada a página completa.
Observe o código abaixo que contém essa verificação para ambas as páginas.
# Arquivo main.py
@app.get("/home/pagina1", response_class=HTMLResponse)
async def pag1(request: Request):
if (not "HX-Request" in request.headers):
return templates.TemplateResponse(request, "index.html")
return templates.TemplateResponse(request, "Pagina1.html")
@app.get("/home/pagina2", response_class=HTMLResponse)
async def pag2(request: Request):
if (not "HX-Request" in request.headers):
return templates.TemplateResponse(request, "index.html")
return templates.TemplateResponse(request, "Pagina2.html")
Agora, ao acessar as URLs /home e /home/paginaX, toda a página será sempre renderizada corretamente.
O último problema que encontramos é que, ao acessar /home/pagina2, a página 1 que é renderizada. Por fim, podemos utilizar uma variável de contexto para indicar para o 'index.html' qual página deve ser renderizada.
Veja abaixo o código final.
# Arquivo main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory=["Templates", "Templates/Partials"])
@app.get("/home",response_class=HTMLResponse)
async def root(request: Request):
return templates.TemplateResponse(request, "index.html", {"pagina": "/home /pagina1"})
@app.get("/home/pagina1", response_class=HTMLResponse)
async def pag1(request: Request):
if (not "HX-Request" in request.headers):
return templates.TemplateResponse(request, "index.html", {"pagina": "/home/pagina1"})
return templates.TemplateResponse(request, "Pagina1.html")
@app.get("/home/pagina2", response_class=HTMLResponse)
async def pag2(request: Request):
if (not "HX-Request" in request.headers):
return templates.TemplateResponse(request, "index.html", {"pagina": "/home/pagina2"})
return templates.TemplateResponse(request, "Pagina2.html")
<!-- Arquivo index.html -->
<DOCTYPE html>
<html lang="pt-br">
<head>
<title>Meu Site</title>
<style>...</style>
<script src="HTMX"></script> <!-- Import do HTMX -->
</head>
<body>
<h1>Meu Site</h1>
<p>Bem vindo ao meu site!</p>
<nav>
<button hx-get="/home/pagina1"
hx-target="main"
hx-push-url="true">
Página 1
</button>
<button hx-get="/home/pagina2"
hx-target="main"
hx-push-url="true">
Página 2
</button>
</nav>
<hr>
<main hx-get="{{ pagina }}"
hx-target="main"
hx-trigger="load"
hx-push-url="true">
</main>
</body>
</html>
Na próxima aula, veremos utilidades mais avançadas do HTMX, mas vamos demonstrar uma dessas funcionalidades agora. Da mesma forma que um click em um botão pode fazer um request, veremos uma forma de fazer requests com atalhos no teclado.
Vamos criar uma nova div vazia no nosso 'index.html' em baixo da 'main'. Nessa div, colocaremos os atributos hx-get="/home/pagina3" e hx-trigger. Dentro do trigger, vamos utilizar o keyup para que a ação necessária para mandar o request seja o pressionar de teclas no teclado. Assim, podemos fazer qualquer combinação de teclas como a mostrada abaixo.
<div hx-get="/home/pagina3"
hx-trigger="keyup[altKey&&(key=='t'||key=='T')] from:body">
</div>
No código acima, para disparar o request da página 3, o usuário deve apertar as teclas Alt+t, com 't' sendo maiúsculo ou minúsculo para prevenir erros com o CapsLock. O from:body indica para o HTMX que essas teclas podem ser pressionadas de qualquer lugar da tela. É possível passar parâmetros para que o atalho apenas funcione quando partes específicas da tela estiverem selecionadas.
Todos os exercícios dessa aula devem ser criados em um mesmo projeto do FastAPI.
Crie um elemento para exibir um contador de curtidas e, ao lado, coloque um botão "Curtir!". O botão deve disparar um request hx-post="/curtir" que fará o número de curtidas aumentar.
Crie um botão para retirar todas as curtidas do seu contador. Antes de fazer o request, o navegador deve emitir um alert pedindo confirmação do usuário para efetuar essa ação. O request também deve ser feito para /curtir.
Não utilize JavaScript (tag <script> ou onclick) para o alert, o HTMX possui um atributo que faz isso. Sua tarefa é descobrir qual atributo do HTMX tem essa função.
Crie um cabeçalho e uma nav com botões para alterar entre as abas do seu site, igual feito em aula. Você deve adicionar a aba das Curtidas (criada nos Exercícios 1 e 2), sua versão do Júpiter e sua página de professor "reformada" como as abas do seu site.
Os botões devem ter algum indicador visual mostrando qual aba está atualmente ativa.
O contador de curtidas deve manter seu valor mesmo que você saia da aba "Curtidas" e volte depois.
Faça com que o atalho de teclado Shift+AltGr+P alterne entre as abas de sua aplicação.
Essa semana não vamos passar exercícios para casa. Sua tarefa será finalizar seu projeto.
As especificações do projeto estão listadas aqui.