Até aqui construímos APIs que retornam JSON. Mas muitas aplicações precisam também servir HTML — painéis administrativos, relatórios, páginas de erro customizadas. Jinja2 é o motor de templates padrão do Flask e do FastAPI, e dominar seus recursos abre essa porta. Na segunda parte do artigo veremos como empacotar toda a aplicação em um container Docker — o padrão da indústria para deploy consistente e reproduzível.
Jinja2: Motor de Templates
pip install jinja2
Sintaxe Fundamental
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>{% block titulo %}Sistema Escolar{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/estilo.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('inicio') }}">Início</a>
<a href="{{ url_for('alunos.listar') }}">Alunos</a>
{% if usuario %}
<span>Olá, {{ usuario.nome }}!</span>
<a href="{{ url_for('auth.logout') }}">Sair</a>
{% else %}
<a href="{{ url_for('auth.login') }}">Entrar</a>
{% endif %}
</nav>
<main>
{% with mensagens = get_flashed_messages(with_categories=true) %}
{% for categoria, mensagem in mensagens %}
<div class="alerta alerta-{{ categoria }}">{{ mensagem }}</div>
{% endfor %}
{% endwith %}
{% block conteudo %}{% endblock %}
</main>
<footer>
<p>© {{ ano }} Sistema Escolar</p>
</footer>
</body>
</html>
<!-- templates/alunos/lista.html -->
{% extends "base.html" %}
{% block titulo %}Lista de Alunos{% endblock %}
{% block conteudo %}
<h1>Alunos</h1>
<p>Total: {{ alunos | length }}</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Nome</th>
<th>Nota</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for aluno in alunos %}
<tr class="{{ 'aprovado' if aluno.nota >= 6 else 'reprovado' }}">
<td>{{ loop.index }}</td>
<td>{{ aluno.nome | title }}</td>
<td>{{ "%.1f" | format(aluno.nota) }}</td>
<td>
{% if aluno.nota >= 9 %}
Excelente
{% elif aluno.nota >= 7 %}
Bom
{% elif aluno.nota >= 6 %}
Regular
{% else %}
Reprovado
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="4">Nenhum aluno cadastrado.</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
Filtros e Funções Jinja2
<!-- Filtros embutidos -->
{{ nome | upper }} <!-- RICARDO -->
{{ nome | lower }} <!-- ricardo -->
{{ nome | title }} <!-- Ricardo Matos -->
{{ nome | truncate(20) }} <!-- Ricardo Matos... -->
{{ lista | length }} <!-- 5 -->
{{ lista | join(", ") }} <!-- a, b, c -->
{{ valor | round(2) }} <!-- 9.50 -->
{{ texto | replace("a", "b") }}
{{ html | safe }} <!-- renderiza HTML sem escapar -->
{{ data | strftime("%d/%m/%Y") }} <!-- filtro customizado -->
<!-- Testes -->
{% if valor is none %}
{% if lista is iterable %}
{% if numero is divisibleby(2) %}
{% if texto is defined %}
Filtros Customizados
# app.py — registrando filtros customizados no Flask
from datetime import datetime
@app.template_filter("data_br")
def filtro_data_br(valor):
if isinstance(valor, str):
valor = datetime.fromisoformat(valor)
return valor.strftime("%d/%m/%Y")
@app.template_filter("moeda")
def filtro_moeda(valor):
return f"R$ {valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
@app.template_filter("plural")
def filtro_plural(numero, singular, plural):
return singular if numero == 1 else plural
<!-- Usando filtros customizados -->
{{ aluno.criado_em | data_br }} <!-- 15/03/2024 -->
{{ produto.preco | moeda }} <!-- R$ 1.299,90 -->
{{ total | plural("aluno", "alunos") }} <!-- 1 aluno / 5 alunos -->
Templates com Flask
from flask import Flask, render_template, request, redirect, url_for, flash
from datetime import datetime
app = Flask(__name__)
app.secret_key = "dev-secret"
alunos_dados = [
{"id": 1, "nome": "Ana Silva", "nota": 9.5},
{"id": 2, "nome": "Bruno Costa", "nota": 7.0},
{"id": 3, "nome": "Carla Souza", "nota": 5.5},
]
@app.context_processor
def variaveis_globais():
"""Variáveis disponíveis em todos os templates."""
return {
"ano": datetime.now().year,
"usuario": None # em produção: sessão do usuário logado
}
@app.route("/")
def inicio():
return render_template("inicio.html")
@app.route("/alunos")
def listar_alunos():
nota_min = request.args.get("nota_min", 0, type=float)
filtrados = [a for a in alunos_dados if a["nota"] >= nota_min]
return render_template(
"alunos/lista.html",
alunos=filtrados,
nota_min=nota_min
)
@app.route("/alunos/novo", methods=["GET", "POST"])
def novo_aluno():
if request.method == "POST":
nome = request.form.get("nome", "").strip()
nota = request.form.get("nota", type=float)
if not nome or nota is None:
flash("Nome e nota são obrigatórios.", "erro")
return render_template("alunos/form.html")
aluno = {"id": len(alunos_dados) + 1, "nome": nome, "nota": nota}
alunos_dados.append(aluno)
flash(f"Aluno '{nome}' cadastrado com sucesso!", "sucesso")
return redirect(url_for("listar_alunos"))
return render_template("alunos/form.html")
Templates com FastAPI
pip install jinja2 python-multipart
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/", response_class=HTMLResponse)
async def inicio(request: Request):
return templates.TemplateResponse(
"inicio.html",
{"request": request, "titulo": "Sistema Escolar"}
)
@app.get("/alunos", response_class=HTMLResponse)
async def listar_alunos(request: Request):
return templates.TemplateResponse(
"alunos/lista.html",
{
"request": request,
"alunos": list(_alunos.values()),
"ano": datetime.now().year
}
)
@app.post("/alunos/novo")
async def criar_aluno_form(
request: Request,
nome: str = Form(...),
email: str = Form(...),
nota: float = Form(...)
):
# Processa o formulário e redireciona
return RedirectResponse(url="/alunos", status_code=303)
Docker: Containerizando a Aplicação
Docker empacota a aplicação com todas as suas dependências em uma unidade isolada — o container — que roda de forma idêntica em qualquer ambiente.
# Instalação — https://docs.docker.com/get-docker/
docker --version
docker compose version
Dockerfile para FastAPI
# Dockerfile
FROM python:3.12-slim
# Variáveis de ambiente
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Diretório de trabalho
WORKDIR /app
# Instala dependências primeiro (aproveitando cache de camadas)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copia o código
COPY . .
# Usuário não-root — boa prática de segurança
RUN adduser --disabled-password --gecos "" appuser && \
chown -R appuser:appuser /app
USER appuser
# Porta exposta
EXPOSE 8000
# Comando de inicialização
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# .dockerignore
__pycache__/
*.pyc
*.pyo
.env
.git/
.gitignore
venv/
.venv/
tests/
*.md
.pytest_cache/
# Construindo a imagem
docker build -t escola-api:1.0.0 .
# Rodando o container
docker run -d \
--name escola-api \
-p 8000:8000 \
-e SECRET_KEY=minha-chave-secreta \
-e DATABASE_URL=postgresql://user:senha@host/db \
escola-api:1.0.0
# Verificando logs
docker logs -f escola-api
# Parando e removendo
docker stop escola-api
docker rm escola-api
Docker Compose: Orquestrando Múltiplos Serviços
# docker-compose.yml
version: "3.9"
services:
api:
build:
context: .
dockerfile: Dockerfile
container_name: escola-api
ports:
- "8000:8000"
environment:
- SECRET_KEY=${SECRET_KEY}
- DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/escola
- REDIS_URL=redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- ./logs:/app/logs
restart: unless-stopped
networks:
- escola-net
db:
image: postgres:16-alpine
container_name: escola-db
environment:
- POSTGRES_DB=escola
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- escola-net
redis:
image: redis:7-alpine
container_name: escola-redis
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- escola-net
nginx:
image: nginx:alpine
container_name: escola-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- api
networks:
- escola-net
volumes:
postgres_data:
redis_data:
networks:
escola-net:
driver: bridge
# nginx/nginx.conf
upstream api {
server api:8000;
}
server {
listen 80;
server_name meusite.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name meusite.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
location / {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
location /static {
alias /app/static;
expires 7d;
}
}
# Arquivo .env para docker-compose
SECRET_KEY=sua-chave-super-secreta-aqui
DB_PASSWORD=senha-do-postgres
REDIS_PASSWORD=senha-do-redis
# Comandos docker compose
docker compose up -d # sobe todos os serviços em background
docker compose down # para e remove containers
docker compose logs -f api # logs em tempo real do serviço api
docker compose ps # status dos serviços
docker compose exec api bash # acessa o container da api
docker compose build --no-cache # reconstrói as imagens
Dockerfile Multi-stage: Imagem Otimizada
# Dockerfile.prod — imagem de produção otimizada
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Copia apenas as dependências instaladas
COPY --from=builder /install /usr/local
# Copia o código da aplicação
COPY app/ ./app/
COPY templates/ ./templates/
COPY static/ ./static/
# Usuário não-root
RUN adduser --disabled-password --gecos "" appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--workers", "4"]
O build multi-stage reduz o tamanho final da imagem significativamente — o stage builder instala compiladores e ferramentas que não precisam ir para produção.
Health Check e Graceful Shutdown
from fastapi import FastAPI
from contextlib import asynccontextmanager
import asyncio
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
print("Conectando ao banco...")
yield
# Shutdown — aguarda requisições em andamento
print("Encerrando conexões...")
await asyncio.sleep(1)
app = FastAPI(lifespan=lifespan)
@app.get("/health")
async def health_check():
"""Endpoint para verificação de saúde — usado pelo Docker e load balancer."""
return {
"status": "healthy",
"versao": "1.0.0",
}
@app.get("/ready")
async def readiness_check():
"""Verifica se a aplicação está pronta para receber tráfego."""
try:
# Verifica conexão com banco, cache, etc.
return {"status": "ready"}
except Exception:
from fastapi import HTTPException
raise HTTPException(status_code=503, detail="Serviço indisponível.")
Resumo
- Jinja2 separa lógica de apresentação com herança de templates, filtros e variáveis de contexto
{% block %}permite herança;{% for %}e{% if %}controlam fluxo no template- Filtros customizados formatam dados específicos do domínio da aplicação
context_processorno Flask injeta variáveis globais em todos os templates- Docker empacota a aplicação e suas dependências em containers portáveis e reproduzíveis
- O
Dockerfiledefine como construir a imagem;.dockerignoreexclui arquivos desnecessários - Docker Compose orquestra múltiplos serviços — API, banco, cache e proxy reverso juntos
- Build multi-stage produz imagens menores separando ambiente de build do de runtime
- Endpoints
/healthe/readypermitem que orquestradores monitorem o estado da aplicação
Referências e Leituras Complementares
- Jinja2 — documentação oficial — https://jinja.palletsprojects.com/en/3.1.x/
- FastAPI — templates — https://fastapi.tiangolo.com/advanced/templates/
- Docker — documentação oficial — https://docs.docker.com/
- Docker Compose — referência — https://docs.docker.com/compose/compose-file/
- Nginx como proxy reverso — https://nginx.org/en/docs/beginners_guide.html
- Python Docker official images — https://hub.docker.com/_/python
- KANE, Sean; MATTHIAS, Karl. Docker: Up and Running. 3. ed. O'Reilly Media, 2023. — guia completo de Docker para desenvolvimento e produção.
- GRINBERG, Miguel. Flask Web Development. 2. ed. O'Reilly Media, 2018. Cap. 3 — templates com Jinja2 no contexto do Flask.