Até agora, vimos o back-end retornando dados puros (JSON). No entanto, existem duas filosofias principais para construir o que o usuário vê na tela:
| Abordagem | Onde a página é montada? | O que o Back-end envia? | Exemplo de Tecnologia |
|---|---|---|---|
| Server-Side Rendering (SSR) | No Servidor (Back-end) | HTML pronto | FastAPI + Jinja2, Django, PHP |
| Client-Side Rendering (CSR) | No Navegador (Front-end) | Dados (JSON) | React, Vue, Angular |
Quando usamos Templating, temos um Back-end Inteligente. O servidor decide exatamente como a página deve ser renderizada e entrega o HTML final. Isso simplifica o front-end, que se torna apenas um "exibidor" de documentos.
Já em frameworks modernos de front-end, o back-end é apenas uma fonte de dados. O front-end é uma aplicação completa que roda no browser do usuário, o que exige gerenciar estado, rotas e lógica em dois lugares diferentes.
HATEOAS (Hypermedia as the Engine of Application State) é um princípio que diz que o cliente deve interagir com a aplicação inteiramente através de hipermídia (links e formulários) fornecidos dinamicamente pelo servidor. Desse ponto em diante do curso veremos como usar templates HTML para aplicar o princípio de HATEOAS para criar interfaces modernas e interativas para aplicativos web de forma simples.
Focar o curso em HATEOAS e SSR foi uma escolha dos membros do CodeLab que planejaram o curso, e ela será melhor explicada em aulas futuras. Por enquanto, saiba que estamos te ensinando os fundamentos para que você possa entender quando usar frameworks de front-end e fugir do padrão histórico pode ser uma boa ideia.
Antes de renderizar arquivos HTML, é essencial criar um diretório para servir arquivos estáticos
(como imagens, CSS e JavaScript do front-end) que poderemos usar com nossos templates.
No FastAPI, usamos o StaticFiles para "montar" (mount) uma pasta do nosso computador em uma rota da nossa API.
O "Mounting" cria automaticamente endpoints do tipo GET para cada arquivo dentro do diretório especificado.
# Arquivo main.py
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# Monta a pasta "static" na rota "/static"
app.mount("/static", StaticFiles(directory="static"), name="static")
O Jinja2 é uma biblioteca python que implementa uma templating engine. Com ele podemos injetar lógica no HTML usando delimitadores específicos para separar o que é código do que é markup a ser renderizado:
{{ variavel }}: Interpolação. Exibe um valor ou resultado de uma expressão.{% instrução %}: Controle. Usado para loops, condicionais e herança.{# comentário #}: Comentários que existem apenas no servidor e não chegam ao browser.Como o Jinja é uma biblioteca à parte, devemos instalá-lo com o pip:
source <nome_do_seu _ambiente_virtual>/bin/activate # para linux
echo jinja2 >> requirements.txt
pip install -r requirements.txt
É sempre bom lembrar de ativar o ambiente virtual e adicionar a dependência aos requisitos do nosso projeto!
Depois de instalar o Jinja no nosso ambiente virtual podemos inicializá-lo no nosso back-end apontando para o diretório de templates de forma direta:
# Arquivo main.py
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
app = FastAPI()
# Sintaxe recomendada: diretório como primeiro argumento posicional
templates = Jinja2Templates(directory="templates")
Permite renderizar blocos de HTML apenas se uma condição for verdadeira (ex: verificar se um usuário é admin).
Rota FastAPI:# Arquivo main.py
@app.get("/perfil")
def ver_perfil(request: Request, logado: bool = False):
user = {"nome": "Rodrigo", "admin": True} if logado else None
return templates.TemplateResponse(
request=request, name="perfil.html", context={"user": user}
)
Template:
<!-- Arquivo perfil.html -->
{% if user %}
<h1>Painel de {{ user.nome }}</h1>
{% if user.admin %}
<span style="color: red;">Acesso Administrativo Ativo</span>
{% endif %}
{% else %}
<p>Por favor, faça login para acessar seu perfil.</p>
{% endif %}
Usado para percorrer listas, dicionários ou qualquer objeto iterável vindo do Python.
Rota FastAPI:# Arquivo main.py
@app.get("/postagens")
def listar_posts(request: Request):
db_posts = ["FastAPI com Jinja2", "Arquitetura REST", "HATEOAS na prática"]
return templates.TemplateResponse(
request=request, name="blog.html", context={"posts": db_posts}
)
Template:
<!-- Arquivo blog.html -->
<ul>
{% for p in posts %}
<li>Artigo: {{ p | title }}</li>
{% else %}
<li>Nenhum artigo publicado ainda.</li>
{% endfor %}
</ul>
O conceito de herança permite criar um "esqueleto" (base) e injetar conteúdos específicos em blocos pré-definidos, mantendo o código DRY (Don't Repeat Yourself).
Base:<!-- Arquivo layout.html -->
<html>
<head><title>Minha Aula de FastAPI</title></head>
<body>
<nav>Cabeçalho Global</nav>
{% block conteudo %}{% endblock %}
<footer>Rodapé Padrão - 2024</footer>
</body>
</html>
Filho:
<!-- Arquivo home.html -->
{% extends "layout.html" %}
{% block conteudo %}
<h2>Início</h2>
<p>Este conteúdo preenche o bloco 'conteudo' definido no layout.html.</p>
{% endblock %}
Na última aula vimos como fazer requests usando a barra de navegação do nosso browser para consultar a API. Agora veremos como implementar interações mais complexas entre o cliente e o back-end através de JavaScript e a nossa API.
O exemplo abaixo mostra como implementar a interação do cliente para o servidor enviando JSON no corpo do request, que é o esperado pelo FastAPI. É possível fazer o FastAPI receber dados de um formulário comum usando Form Data.
Primeiro, definimos um modelo usando Pydantic para validar os dados que chegam e uma rota POST para recebê-los.
# Arquivo main.py
from pydantic import BaseModel
class Usuario(BaseModel):
nome: str
bio: str
usuarios_db = []
@app.post("/usuarios")
def criar_usuario(user: Usuario):
usuarios_db.append(user.dict())
return {"usuario": user.nome}
No lado do cliente, utilizamos a função fetch(). Precisamos configurar três coisas principais:
'POST'.application/json.JSON.stringify().
async function enviarUsuario() {
const dados = {
nome: document.getElementById('nome').value,
bio: document.getElementById('bio').value
};
const resposta = await fetch('/usuarios', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(dados)
});
if (resposta.ok) {
const resultado = await resposta.json();
alert("Usuário " + resultado.usuario + " criado!");
} else {
alert("Erro ao enviar!");
}
}
Note que o botão usa type="button" e chama nossa função, em vez de um type="submit" tradicional que recarregaria a página.
<form>
<input type="text" id="nome" placeholder="Nome">
<input type="text" id="bio" placeholder="Bio">
<button type="button" onclick="enviarUsuario()">Salvar via JS</button>
</form>
Ponto de atenção: Quando usamos fetch, o back-end geralmente responde com JSON. Se você quiser atualizar a tela após o envio, precisará usar o JavaScript para manipular o DOM (ex: adicionar um novo <li> na lista).
Imagine um Middleware como uma camada de software que envolve toda a sua aplicação. Ele intercepta cada requisição que chega ao servidor antes de ela atingir a rota, e também intercepta a resposta antes de ela ser enviada ao cliente.
Isso é extremamente útil para tarefas transversais (cross-cutting concerns), como:
No exemplo abaixo, usamos o call_next, que é uma função que "passa a bola" para a próxima etapa (seja outro middleware ou a rota final).
import time
import logging
# Configuração básica de log para aparecer no terminal
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("API")
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
# 1. Código executado ANTES da rota
start_time = time.perf_counter()
# 2. A requisição viaja até a rota e volta como resposta
response = await call_next(request)
# 3. Código executado DEPOIS da rota
process_time = time.perf_counter() - start_time
# Adicionamos um header customizado na resposta para o cliente ver
response.headers["X-Process-Time"] = str(process_time)
logger.info(f"Rota: {request.url.path} | Tempo: {process_time:.4f}s")
return response
O sistema de Dependency Injection do FastAPI permite que você extraia lógica repetitiva (como autenticação ou conexão com banco de dados) e a "injete" apenas nas rotas que precisam dela.
A função Depends() atua como um gatekeeper (porteiro): se a função de dependência falhar ou lançar uma exceção, a rota principal nem sequer começa a ser executada.
Diferente dos Query Parameters, os Cookies são pequenos dados que o servidor pede para o navegador guardar. Uma vez guardado, o navegador envia esse cookie automaticamente em todas as próximas requisições para o mesmo site.
Vamos usar o Cookie do FastAPI para criar uma dependência que identifica o usuário logado:
# Arquivo main.py
from fastapi import Depends, HTTPException, status, Cookie, Response
from typing import Annotated
# Nossa base de dados em memória
users_db = [
{"username": "joão", "bio": "Professor de Python"},
{"username": "maria", "bio": "Desenvolvedora Web"},
]
# 1. Rota para "Logar" (Define o Cookie)
@app.post("/login")
def login(username: str, response: Response):
# Buscamos o usuário usando um laço simples
usuario_encontrado = None
for u in users_db:
if u["username"] == username:
usuario_encontrado = u
break
if not usuario_encontrado:
raise HTTPException(status_code=404, detail="Usuário não encontrado")
# O servidor diz ao navegador: "Guarde esse nome no cookie 'session_user'"
response.set_cookie(key="session_user", value=username)
return {"message": "Logado com sucesso"}
# 2. A Dependência: Lendo o Cookie
def get_active_user(session_user: Annotated[str | None, Cookie()] = None):
# O FastAPI busca automaticamente um cookie chamado 'session_user'
if not session_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Acesso negado: você não está logado."
)
user = next((u for u in users_db if u["username"] == session_user), None)
if not user:
raise HTTPException(status_code=401, detail="Sessão inválida")
return user
# 3. Rota Protegida
@app.get("/profile")
def show_profile(request: Request, user: dict = Depends(get_active_user)):
return templates.TemplateResponse(
request=request,
name="profile.html",
context={"username": user["username"], "bio": user["bio"]}
)
Aviso de Segurança: Em aplicações reais, nunca guardamos o nome de usuário puro em um cookie (isso permitiria que qualquer um trocasse o nome no navegador e fingisse ser outra pessoa). Usamos Tokens Assinados (JWT) ou IDs de sessão aleatórios. Para saber mais, veja a seção de Segurança da documentação oficial.
Você pode criar dependências que dependem de outras. Por exemplo, uma dependência get_admin_user poderia chamar get_active_user e, após receber o dicionário do usuário, verificar se ele tem permissões de administrador antes de liberar a rota.
Implemente uma versão do exemplo de autenticação acima:
Não é necessário implementar um esquema realmente seguro de autenticação!!!
Dicas de possível implementação:
@app.get("/") retorna um template com o formulário de criação de usuário@app.post("users") rota que vai criar usuários novos a partir do formulário@app.get("login") retorna um template com o formulário de login@app.post("login") recebe dados do formulário de login e retorna cookie de sessão@app.get("home") rota protegida com o depends que depende do cookie de sessão