Aula 5: Avançando com FastAPI

Renderização

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

O Back-end Inteligente vs. Front-end Inteligente

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 e a Web como ela foi pensada

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.

Arquivos Estáticos

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")
        

Templating com Jinja2

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:

1. Configuração

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")
        

2. Desvios Condicionais

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 %}
        

3. For loop

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>
        

4. Herança de Templates (Extends/Block)

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 %}
        

Fazendo requests

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.

1. O Back-end

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}
        

2. O Front-end

No lado do cliente, utilizamos a função fetch(). Precisamos configurar três coisas principais:


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!");
    }
}
        

3. Formulário HTML

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).

Middleware: O "Filtro" Global

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:

Exemplo: Logger de Performance

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
        

Injeção de Dependências (Depends)

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.

Simulando uma Sessão com Cookies

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.

Dependências em Cascata

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.

Exercícios

Exercício de Sala

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: