DevTools

Cheatsheet Flask

Micro-framework Python leve e flexível para aplicações web

Voltar às linguagens
Flask
72 cards encontrados
Categorias:
Versões:

Setup


9 cards
Instalar Flask
# Criar ambiente virtual:
python -m venv venv
source venv/bin/activate   # Linux/Mac
venv\Scripts\activate      # Windows

# Instalar:
pip install flask

# Verificar:
flask --version
# Python 3.12, Flask 3.1, Werkzeug 3.1

venv isola dependências do projeto. pip install flask instala o framework e dependências (Werkzeug, Jinja2). Sempre ativar o ambiente antes de trabalhar.

Configuração
# config.py
import os

class Config:
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev")
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        "DATABASE_URL", "sqlite:///app.db"
    )
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevConfig(Config):
    DEBUG = True

class ProdConfig(Config):
    DEBUG = False

config = {
    "development": DevConfig,
    "production": ProdConfig,
    "default": DevConfig,
}

SECRET_KEY é obrigatório para sessões e CSRF. os.environ.get() lê variáveis de ambiente. Classes de config por ambiente (Dev, Prod). SQLALCHEMY_TRACK_MODIFICATIONS = False evita overhead.

Flask CLI
# Comandos úteis:
flask routes              # listar todas as rotas
flask shell               # shell interativo
flask db upgrade          # aplicar migrations
flask db migrate -m "msg" # gerar migration

# Comandos custom:
import click

@app.cli.command("seed")
def seed():
    """Popular base de dados."""
    db.session.add(User(nome="Admin"))
    db.session.commit()
    click.echo("Dados criados!")

# Executar:
flask seed

flask routes lista endpoints registados. flask shell abre REPL com app carregada. @app.cli.command() cria comandos custom. click.echo() imprime no terminal. Útil para seeds, limpeza e tarefas admin.

App mínima (Hello World)
# app.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def inicio():
    return "Olá, Flask!"

if __name__ == "__main__":
    app.run(debug=True)

Flask(__name__) cria a aplicação. @app.route() define o endpoint. app.run(debug=True) inicia o servidor com auto-reload. __name__ ajuda o Flask a localizar templates e static.

Variáveis de ambiente
# .env (usar python-dotenv)
FLASK_APP=run.py
FLASK_DEBUG=1
SECRET_KEY=super-secreto-123
DATABASE_URL=postgresql://user:pass@localhost/db

# Carregar automaticamente:
pip install python-dotenv

# Flask carrega .env automaticamente!
# Ou manual:
from dotenv import load_dotenv
load_dotenv()

# Aceder:
import os
key = os.environ["SECRET_KEY"]

FLASK_APP indica o módulo da app. FLASK_DEBUG=1 ativa debug. Flask 2.x+ carrega .env automaticamente com python-dotenv. Nunca commitar .env (adicionar ao .gitignore).

Estrutura do projeto
projeto/
├── app/
│   ├── __init__.py      # app factory
│   ├── models.py        # modelos SQLAlchemy
│   ├── routes.py        # rotas principais
│   ├── templates/       # HTML Jinja2
│   │   ├── base.html
│   │   └── index.html
│   └── static/          # CSS, JS, imagens
├── config.py            # configurações
├── requirements.txt     # dependências
└── run.py               # ponto de entrada

templates/ e static/ são localizados automaticamente pelo Flask. __init__.py contém o factory. config.py separa configurações. requirements.txt lista dependências para reprodução.

Executar o servidor
# Modo recomendado (Flask CLI):
flask run
flask run --debug          # com debug
flask run --port 8080      # porta custom
flask run --host 0.0.0.0   # acessível na rede

# Alternativa (script):
python run.py

# Com auto-reload e debug:
export FLASK_DEBUG=1
flask run

# Servidor de produção (NUNCA usar flask run):
gunicorn -w 4 "app:create_app()"

flask run é o comando padrão. --debug ativa debugger e reload. --host 0.0.0.0 expõe na rede local. Em produção usar gunicorn ou uwsgi — o servidor do Flask é só para desenvolvimento.

Application Factory
# app/__init__.py
from flask import Flask

def create_app(config_name="default"):
    app = Flask(__name__)
    app.config.from_object(config[config_name])

    # Inicializar extensões
    db.init_app(app)
    migrate.init_app(app, db)
    login.init_app(app)

    # Registar blueprints
    from .routes import bp
    app.register_blueprint(bp)

    return app

create_app() é o padrão factory — permite múltiplas instâncias (testes, produção). Extensões são inicializadas com init_app(). Blueprints registados dentro do factory. Essencial para projetos médios/grandes.

Extensões essenciais
# Instalar extensões comuns:
pip install flask-sqlalchemy    # ORM
pip install flask-migrate       # migrations
pip install flask-login         # autenticação
pip install flask-wtf           # formulários
pip install flask-cors          # CORS
pip install flask-caching       # cache
pip install flask-mail          # emails
pip install flask-restful       # API REST

# requirements.txt:
flask>=3.0
flask-sqlalchemy>=3.1
flask-migrate>=4.0
flask-login>=0.6

Flask é minimalista — extensões adicionam funcionalidade. flask-sqlalchemy = ORM. flask-migrate = migrations (Alembic). flask-login = sessões de utilizador. flask-wtf = forms com CSRF. Sempre fixar versões no requirements.txt.

Rotas e Views


9 cards
Rotas básicas
@app.route("/")
def inicio():
    return "Página inicial"

@app.route("/sobre")
def sobre():
    return render_template("sobre.html")

# Múltiplas rotas para mesma view:
@app.route("/ola")
@app.route("/ola/<nome>")
def ola(nome="Mundo"):
    return f"Olá, {nome}!"

@app.route() mapeia URL para função. O nome da função é o endpoint (usado no url_for). Múltiplos decoradores = múltiplas URLs. Valor default no parâmetro torna-o opcional.

url_for e redirect
from flask import url_for, redirect

# Gerar URLs (nunca hardcode!):
url_for("inicio")                    # "/"
url_for("perfil", username="ana")    # "/user/ana"
url_for("post", id=5)                # "/post/5"

# Com query string:
url_for("search", q="flask", page=2)
# "/search?q=flask&page=2"

# Redirecionar:
return redirect(url_for("login"))
return redirect(url_for("perfil", username="ana"))

# Redirect com código:
return redirect(url_for("inicio"), code=301)

url_for() gera URLs a partir do nome da função — se a rota mudar, o código continua válido. redirect() retorna 302 por default. code=301 para redirect permanente. Sempre usar url_for em vez de strings hardcoded.

Subdomínios e regras
# Ativar subdomínios:
app.config["SERVER_NAME"] = "meusite.com"

@app.route("/", subdomain="<user>")
def perfil_sub(user):
    return f"Perfil de {user}"
# ana.meusite.com → "Perfil de ana"

# Regras avançadas:
from werkzeug.routing import Rule

app.url_map.add(Rule(
    "/api/<version>/users",
    endpoint="users",
    defaults={"version": "v1"}
))

# Converter custom:
from werkzeug.routing import BaseConverter

class RegexConverter(BaseConverter):
    def __init__(self, map, *items):
        super().__init__(map)
        self.regex = items[0]

subdomain="<user>" captura subdomínios (requer SERVER_NAME). Rule permite regras avançadas. Conversores custom com BaseConverter e regex. Útil para multi-tenancy e APIs versionadas.

Métodos HTTP
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        email = request.form["email"]
        # autenticar...
        return redirect(url_for("dashboard"))
    return render_template("login.html")

# API REST:
@app.route("/api/users", methods=["GET"])
def listar(): ...

@app.route("/api/users", methods=["POST"])
def criar(): ...

@app.route("/api/users/<int:id>", methods=["PUT", "DELETE"])
def atualizar(id): ...

methods=["GET", "POST"] define métodos aceites. Default é só GET. request.method indica o método usado. Para APIs: GET=ler, POST=criar, PUT=atualizar, DELETE=remover.

Error handlers
@app.errorhandler(404)
def nao_encontrado(e):
    return render_template("404.html"), 404

@app.errorhandler(500)
def erro_interno(e):
    return render_template("500.html"), 500

@app.errorhandler(403)
def acesso_negado(e):
    return "Acesso negado!", 403

# Erro custom com abort:
from flask import abort

@app.route("/admin")
def admin():
    if not current_user.is_admin:
        abort(403)
    return "Painel admin"

@app.errorhandler(código) personaliza páginas de erro. abort(código) levanta erro HTTP manualmente. Retornar tupla (template, código) define o status. Útil para 404, 403, 500 customizados com layout da app.

Parâmetros de URL
# String (default):
@app.route("/user/<username>")
def perfil(username):
    return f"Perfil de {username}"

# Conversores de tipo:
@app.route("/post/<int:id>")
def post(id):          # id é int
    ...

@app.route("/price/<float:valor>")
def preco(valor):      # valor é float
    ...

@app.route("/path/<path:subpath>")
def ficheiro(subpath): # aceita /
    ...

# Múltiplos parâmetros:
@app.route("/blog/<int:ano>/<slug>")
def artigo(ano, slug): ...

Conversores: int, float, path (aceita barras), string (default). Se o tipo não corresponder → 404 automático. Múltiplos parâmetros separados por /. O nome deve corresponder ao argumento da função.

Before/After request
@app.before_request
def antes():
    # Executa ANTES de cada request
    g.inicio = time.time()
    if request.endpoint != "static":
        # log, auth, etc.
        pass

@app.after_request
def depois(response):
    # Executa DEPOIS (modifica response)
    duracao = time.time() - g.inicio
    response.headers["X-Duracao"] = f"{duracao:.3f}s"
    return response

@app.teardown_request
def finalizar(exc):
    # Sempre executa (mesmo com erro)
    pass

@app.before_request executa antes de cada request (auth, logging). @app.after_request modifica a response (headers, cache). @app.teardown_request executa sempre (cleanup). g é um objeto global por-request.

Query parameters
from flask import request

# URL: /search?q=flask&page=2&lang=pt
@app.route("/search")
def search():
    q = request.args.get("q", "")
    page = request.args.get("page", 1, type=int)
    lang = request.args.get("lang", "pt")

    # Todos os params:
    todos = request.args.to_dict()
    # {"q": "flask", "page": "2", "lang": "pt"}

    # Múltiplos valores (lista):
    # /tags?a=1&a=2
    tags = request.args.getlist("a")
    # ["1", "2"]

    return f"Busca: {q}, página {page}"

request.args é um dicionário imutável. .get(chave, default, type=) com conversão automática. .getlist() para parâmetros repetidos. .to_dict() converte tudo. Valores são sempre strings sem type=.

Class-based views
from flask.views import MethodView

class UserAPI(MethodView):
    def get(self, user_id=None):
        if user_id is None:
            return {"users": ["ana", "joão"]}
        return {"id": user_id}

    def post(self):
        data = request.json
        return {"criado": data}, 201

    def delete(self, user_id):
        return "", 204

# Registar:
app.add_url_rule(
    "/api/users",
    view_func=UserAPI.as_view("users"),
    defaults={"user_id": None}
)
app.add_url_rule(
    "/api/users/<int:user_id>",
    view_func=UserAPI.as_view("user_detail")
)

MethodView separa métodos HTTP em métodos da classe. Cada método (get, post, delete) trata o verbo correspondente. add_url_rule() regista. Ideal para APIs REST com lógica complexa por método.

Templates Jinja2


9 cards
Renderizar templates
from flask import render_template, render_template_string

# Ficheiro HTML:
@app.route("/posts")
def posts():
    lista = [{"titulo": "Post 1"}, {"titulo": "Post 2"}]
    return render_template(
        "posts.html",
        titulo="Meus Posts",
        posts=lista,
        total=len(lista)
    )

# String inline (cuidado com XSS!):
return render_template_string(
    "Olá {{ nome }}!", nome="Ana"
)

render_template() procura em templates/. Passar variáveis como keyword arguments. render_template_string() para strings (evitar com input do utilizador — risco XSS). Dados ficam disponíveis como variáveis no template.

Loops
{% for post in posts %}
    <li>{{ loop.index }}. {{ post.titulo }}</li>
{% endfor %}

<!-- Variáveis do loop: -->
{{ loop.index }}     <!-- 1, 2, 3... -->
{{ loop.index0 }}    <!-- 0, 1, 2... -->
{{ loop.first }}     <!-- True no 1º -->
{{ loop.last }}      <!-- True no último -->
{{ loop.length }}    <!-- total -->

<!-- Else (lista vazia): -->
{% for item in itens %}
    <p>{{ item }}</p>
{% else %}
    <p>Sem itens.</p>
{% endfor %}

<!-- Dict: -->
{% for chave, valor in dict.items() %}
    {{ chave }}: {{ valor }}
{% endfor %}

{% for %} itera listas/dicts. loop.index (1-based), loop.first, loop.last são variáveis especiais. {% else %} executa se a lista estiver vazia. .items() para dicionários.

Segurança em templates
<!-- Jinja2 escapa HTML automaticamente: -->
{{ user_input }}
<!-- <script> → &lt;script&gt; (seguro!) -->

<!-- DESATIVAR escape (cuidado!): -->
{{ html_content | safe }}
<!-- Renderiza HTML real — só para conteúdo
     de confiança (ex: rich text admin) -->

<!-- Autoescape por extensão: -->
<!-- .html → auto-escape ON -->
<!-- .txt → auto-escape OFF -->

<!-- Configurar globalmente: -->
app.jinja_env.autoescape = True

<!-- Nunca fazer: -->
<!-- {{ request.args.get("nome") | safe }} -->
<!-- ↑ XSS garantido! -->

Jinja2 faz auto-escape por default em .html — previne XSS. | safe desativa escape (só para conteúdo confiável). Nunca aplicar safe a input do utilizador. Markup() marca strings como seguras no Python.

Variáveis e expressões
<!-- {{ }} para output -->
<h1>{{ titulo }}</h1>
<p>{{ user.nome }}</p>
<p>{{ lista[0] }}</p>
<p>{{ dict["chave"] }}</p>

<!-- Expressões -->
<p>{{ 2 + 3 }}</p>
<p>{{ "Olá " ~ nome }}</p>
<p>{{ lista | length }} itens</p>

<!-- Comentários -->
{# Isto não aparece no HTML #}

<!-- Whitespace control -->
{%- if ativo -%}
  Ativo
{%- endif -%}

{{ }} imprime variáveis. Ponto para atributos/keys: user.nome. ~ concatena strings. {# #} são comentários. {%- -%} remove whitespace extra. Expressões Python básicas são suportadas.

Herança de templates
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block titulo %}App{% endblock %}</title>
</head>
<body>
    {% include "navbar.html" %}
    <main>
        {% block conteudo %}{% endblock %}
    </main>
    {% block scripts %}{% endblock %}
</body>
</html>

<!-- templates/index.html -->
{% extends "base.html" %}

{% block titulo %}Início{% endblock %}

{% block conteudo %}
    <h1>Página inicial</h1>
{% endblock %}

{% extends %} herda de um template base. {% block %} define secções sobrescrevíveis. O filho só redefine os blocks que precisa. {{ super() }} inclui conteúdo do block pai. Padrão essencial para layouts consistentes.

Filtros
<!-- Filtros com pipe | -->
{{ nome | upper }}           <!-- ANA -->
{{ nome | lower }}           <!-- ana -->
{{ nome | capitalize }}      <!-- Ana -->
{{ nome | title }}           <!-- Ana Silva -->
{{ texto | truncate(50) }}   <!-- corta a 50 chars -->
{{ lista | length }}         <!-- tamanho -->
{{ lista | join(", ") }}     <!-- "a, b, c" -->
{{ valor | round(2) }}       <!-- 3.14 -->
{{ data | datetimeformat }}  <!-- custom -->
{{ html | safe }}            <!-- sem escape -->
{{ preco | default("N/A") }} <!-- se None -->

<!-- Encadear: -->
{{ nome | trim | upper }}

Filtros transformam output com |. safe desativa escape HTML (cuidado!). default() para valores nulos. truncate() corta texto. Encadear com múltiplos |. Filtros custom registados com @app.template_filter().

Include e macros
<!-- Include (inserir parcial): -->
{% include "partials/navbar.html" %}
{% include "partials/footer.html" %}

<!-- Com variáveis: -->
{% include "card.html" with context %}

<!-- Macros (funções reutilizáveis): -->
{% macro input(name, label, type="text") %}
<div class="field">
    <label for="{{ name }}">{{ label }}</label>
    <input type="{{ type }}" name="{{ name }}"
           id="{{ name }}">
</div>
{% endmacro %}

<!-- Usar macro: -->
{{ input("email", "Email", type="email") }}
{{ input("senha", "Senha", type="password") }}

{% include %} insere parciais (navbar, footer). {% macro %} cria componentes reutilizáveis com parâmetros. Macros aceitam defaults. Importar de outro ficheiro: {% from "forms.html" import input %}.

Condicionais
{% if user %}
    <p>Olá, {{ user.nome }}</p>
{% elif user_convidado %}
    <p>Bem-vindo, convidado!</p>
{% else %}
    <p>Faça <a href="/login">login</a></p>
{% endif %}

<!-- Operadores: -->
{% if idade >= 18 and ativo %}
{% if role in ["admin", "mod"] %}
{% if lista is defined %}
{% if valor is none %}
{% if nome is not none %}

<!-- Expressão inline: -->
<p>{{ "Admin" if user.is_admin else "User" }}</p>

{% if %} / {% elif %} / {% else %} / {% endif %}. Operadores: and, or, not, in. Testes: is defined, is none. Expressão ternária: {{ X if cond else Y }}.

Filtros e testes custom
# Filtro custom:
@app.template_filter("moeda")
def filtro_moeda(valor):
    return f"€{valor:,.2f}"

# No template: {{ preco | moeda }} → €1.234,56

# Filtro com parâmetro:
@app.template_filter("truncate_words")
def truncate_words(texto, n=20):
    words = texto.split()[:n]
    return " ".join(words) + "..."

# Teste custom:
@app.template_test("par")
def teste_par(n):
    return n % 2 == 0

# No template: {% if n is par %}

@app.template_filter("nome") regista filtro custom. Usar com {{ valor | nome }}. @app.template_test() cria testes para {% if x is teste %}. Filtros recebem o valor como 1º argumento + parâmetros extras.

Request e Response


9 cards
Dados do request
from flask import request

@app.route("/dados", methods=["POST"])
def dados():
    # Form data (application/x-www-form-urlencoded):
    email = request.form["email"]
    nome = request.form.get("nome", "")

    # JSON body:
    data = request.json          # dict
    data = request.get_json()    # alternativo
    data = request.get_json(silent=True)  # None se inválido

    # Ficheiros:
    foto = request.files["foto"]

    # Info do request:
    request.method       # "POST"
    request.url          # URL completa
    request.remote_addr  # IP do cliente
    request.content_type # mime type

request.form para dados de formulário. request.json / get_json() para body JSON. request.files para uploads. request.method indica o verbo. Usar .get() com default para evitar KeyError.

Sessões
from flask import session

# Requer SECRET_KEY configurado!
app.config["SECRET_KEY"] = "super-secreto"

@app.route("/login", methods=["POST"])
def login():
    if autenticar(request.form):
        session["user_id"] = user.id
        session["nome"] = user.nome
        session.permanent = True  # usa PERMANENT_SESSION_LIFETIME
        return redirect(url_for("dashboard"))
    return "Credenciais inválidas", 401

@app.route("/logout")
def logout():
    session.clear()       # limpar tudo
    session.pop("user_id", None)  # ou só uma chave
    return redirect(url_for("inicio"))

# Verificar:
if "user_id" in session: ...

session armazena dados no cookie assinado (cliente). Requer SECRET_KEY. session.permanent = True usa tempo de vida configurado. session.clear() limpa tudo. Dados são serializados — só tipos JSON-safe. Não guardar objetos complexos.

CORS e caching
# CORS (Cross-Origin Resource Sharing):
pip install flask-cors

from flask_cors import CORS

CORS(app)                    # todas as rotas
CORS(app, resources={
    r"/api/*": {"origins": "https://meusite.com"}
})

# Cache com flask-caching:
pip install flask-caching

from flask_caching import Cache
cache = Cache(app, config={"CACHE_TYPE": "simple"})

@app.route("/dados")
@cache.cached(timeout=300)   # 5 minutos
def dados():
    return jsonify(dados_pesados())

# Invalidar:
cache.delete("dados")

flask-cors permite requests de outros domínios. Configurar origins para restringir. flask-caching com @cache.cached(timeout=N) evita recálculos. Tipos: simple (memória), redis, memcached. Essencial para APIs públicas.

Respostas JSON (API)
from flask import jsonify

@app.route("/api/users")
def users():
    return jsonify({
        "users": [
            {"id": 1, "nome": "Ana"},
            {"id": 2, "nome": "João"},
        ],
        "total": 2
    })

# Com status code:
return jsonify({"erro": "Não encontrado"}), 404

# Lista direta (Flask 2.2+):
return [{"id": 1}, {"id": 2}]

# Dict direto (Flask 2.2+):
return {"status": "ok"}

# Headers custom:
resp = jsonify({"ok": True})
resp.headers["X-Total"] = "42"
return resp

jsonify() serializa dict/lista para JSON com Content-Type: application/json. Flask 2.2+ permite retornar dict/lista diretamente. Tupla (json, código) define status. Ideal para APIs REST.

Upload de ficheiros
from werkzeug.utils import secure_filename
import os

UPLOAD_DIR = "uploads"
EXTENSOES = {"png", "jpg", "pdf"}

@app.route("/upload", methods=["POST"])
def upload():
    f = request.files["ficheiro"]

    if not f or f.filename == "":
        return "Sem ficheiro", 400

    ext = f.filename.rsplit(".", 1)[1].lower()
    if ext not in EXTENSOES:
        return "Tipo não permitido", 400

    nome = secure_filename(f.filename)
    f.save(os.path.join(UPLOAD_DIR, nome))
    return f"Enviado: {nome}", 201

# HTML: <form enctype="multipart/form-data">

request.files["campo"] acede ao ficheiro. secure_filename() remove caracteres perigosos. Validar extensão e tamanho. f.save() guarda no disco. Formulário precisa de enctype="multipart/form-data". Configurar MAX_CONTENT_LENGTH para limite.

Headers e status code
from flask import make_response

# Status code simples:
return "Criado!", 201
return "Sem conteúdo", 204
return "Erro", 400

# Response completo:
resp = make_response("OK")
resp.status_code = 200
resp.headers["X-Custom"] = "valor"
resp.headers["Cache-Control"] = "no-cache"
resp.content_type = "text/plain"
return resp

# Tuple com headers:
return "Erro", 400, {"X-Erro": "validacao"}

# Redirect:
return "", 302, {"Location": "/login"}

Retornar tupla: (body, status) ou (body, status, headers). make_response() cria objeto editável. resp.headers[] adiciona headers. Códigos: 200=OK, 201=Criado, 204=Sem conteúdo, 400=Bad request, 404=Not found.

Download e streaming
from flask import send_file, send_from_directory, Response

# Enviar ficheiro:
@app.route("/download/<nome>")
def download(nome):
    return send_from_directory("uploads", nome,
                               as_attachment=True)

# Gerar e enviar:
@app.route("/export")
def export():
    return send_file(
        "relatorio.pdf",
        mimetype="application/pdf",
        as_attachment=True,
        download_name="relatorio.pdf"
    )

# Streaming (ficheiros grandes):
@app.route("/stream")
def stream():
    def gerar():
        for i in range(1000):
            yield f"linha {i}\n"
    return Response(gerar(), mimetype="text/plain")

send_from_directory() envia ficheiro de forma segura (previne path traversal). as_attachment=True força download. Response(generator()) para streaming. mimetype define o tipo. Ideal para exports e ficheiros grandes.

Cookies
from flask import request, make_response

# Definir cookie:
resp = make_response("Cookie definido")
resp.set_cookie(
    "tema", "escuro",
    max_age=3600,          # 1 hora
    httponly=True,         # sem acesso JS
    secure=True,           # só HTTPS
    samesite="Lax"         # proteção CSRF
)
return resp

# Ler cookie:
tema = request.cookies.get("tema", "claro")

# Remover cookie:
resp = make_response("Removido")
resp.delete_cookie("tema")
return resp

set_cookie() define com opções de segurança. httponly=True impede acesso via JavaScript. secure=True só envia por HTTPS. samesite protege contra CSRF. request.cookies.get() lê. Preferir sessões para dados sensíveis.

Flash messages
from flask import flash, get_flashed_messages

@app.route("/salvar", methods=["POST"])
def salvar():
    flash("Dados salvos com sucesso!", "sucesso")
    flash("Email já registado.", "erro")
    return redirect(url_for("perfil"))

# No template (base.html):
# {% with messages = get_flashed_messages(
#     with_categories=true) %}
#   {% for categoria, msg in messages %}
#     <div class="alert-{{ categoria }}">
#       {{ msg }}
#     </div>
#   {% endfor %}
# {% endwith %}

# Categorias: success, error, warning, info

flash(msg, categoria) guarda mensagem para o próximo request. get_flashed_messages(with_categories=true) recupera no template. Mensagens são consumidas (aparecem uma vez). Requer SECRET_KEY. Padrão para feedback pós-redirect.

Base de Dados


9 cards
Setup Flask-SQLAlchemy
pip install flask-sqlalchemy

# app/__init__.py
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    app.config["SQLALCHEMY_DATABASE_URI"] = \
        "sqlite:///app.db"
    db.init_app(app)
    return app

# Criar tabelas (dev apenas!):
with app.app_context():
    db.create_all()

SQLAlchemy() cria a instância. db.init_app(app) liga à aplicação (factory pattern). SQLALCHEMY_DATABASE_URI define a BD. db.create_all() cria tabelas (usar migrations em produção). Requer app_context().

Relações
class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    comentarios = db.relationship(
        "Comentario", backref="post",
        lazy="dynamic", cascade="all, delete-orphan"
    )

class Comentario(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    texto = db.Column(db.Text)
    post_id = db.Column(
        db.Integer, db.ForeignKey("post.id"),
        nullable=False
    )

# Usar:
post.comentarios.all()
post.comentarios.filter_by(aprovado=True)
comentario.post.titulo  # backref

db.relationship() define a relação. backref="post" cria acesso inverso. db.ForeignKey() na coluna da tabela filha. lazy="dynamic" retorna query (filtros). cascade="all, delete-orphan" apaga filhos ao eliminar pai.

Raw SQL e transações
from sqlalchemy import text

# Query SQL direta:
result = db.session.execute(
    text("SELECT * FROM posts WHERE views > :min"),
    {"min": 100}
)
for row in result:
    print(row.titulo)

# Transação manual:
try:
    db.session.begin_nested()  # SAVEPOINT
    db.session.add(Post(titulo="A"))
    db.session.add(Post(titulo="B"))
    db.session.commit()
except Exception:
    db.session.rollback()
    raise

# Context manager (Flask 2.x):
with db.session.begin():
    db.session.add(post)
    # commit automático (ou rollback)

db.session.execute(text(...)) para SQL raw com parâmetros seguros. begin_nested() cria SAVEPOINT. rollback() reverte em caso de erro. with db.session.begin() faz commit/rollback automático. Evitar SQL raw — preferir ORM.

Definir modelos
from datetime import datetime

class Post(db.Model):
    __tablename__ = "posts"

    id = db.Column(db.Integer, primary_key=True)
    titulo = db.Column(db.String(200), nullable=False)
    corpo = db.Column(db.Text, default="")
    views = db.Column(db.Integer, default=0)
    publicado = db.Column(db.Boolean, default=False)
    criado = db.Column(db.DateTime, default=datetime.utcnow)
    preco = db.Column(db.Float)

    def __repr__(self):
        return f"<Post {self.titulo}>"

# Tipos: Integer, String, Text, Boolean,
#         DateTime, Float, JSON, LargeBinary

db.Model é a classe base. db.Column(tipo, opções) define campos. nullable=False = obrigatório. default= valor por omissão. __tablename__ personaliza o nome da tabela. Tipos comuns: String, Integer, Text, Boolean, DateTime.

Migrations (Flask-Migrate)
pip install flask-migrate

# Setup:
from flask_migrate import Migrate
migrate = Migrate(app, db)

# Comandos CLI:
flask db init          # criar pasta migrations/
flask db migrate -m "add tabela posts"
flask db upgrade       # aplicar
flask db downgrade     # reverter
flask db history       # histórico
flask db current       # versão atual

# Workflow:
# 1. Modificar modelo
# 2. flask db migrate -m "descrição"
# 3. Revisar ficheiro gerado
# 4. flask db upgrade

flask-migrate usa Alembic por baixo. db init só uma vez. db migrate gera script de migração. db upgrade aplica. Sempre revisar o ficheiro gerado antes de aplicar. Essencial para evolução do esquema em produção.

CRUD básico
# CREATE:
post = Post(titulo="Olá", corpo="Texto")
db.session.add(post)
db.session.commit()

# READ:
post = Post.query.get(1)           # por PK
post = db.session.get(Post, 1)     # alternativo
todos = Post.query.all()
primeiro = Post.query.first()

# UPDATE:
post.titulo = "Novo título"
db.session.commit()

# DELETE:
db.session.delete(post)
db.session.commit()

# Bulk:
db.session.add_all([post1, post2, post3])
db.session.commit()

db.session.add() + commit() para criar. query.get(id) busca por PK. Modificar atributos + commit() para atualizar. db.session.delete() remove. Sempre commit() para persistir. rollback() para cancelar.

Paginação
@app.route("/posts")
def posts():
    page = request.args.get("page", 1, type=int)
    per_page = 20

    paginacao = Post.query.order_by(
        Post.criado.desc()
    ).paginate(
        page=page, per_page=per_page,
        error_out=False
    )

    posts = paginacao.items       # lista da página
    total = paginacao.total       # total de registos
    tem_proxima = paginacao.has_next
    tem_anterior = paginacao.has_prev

    return render_template("posts.html",
        posts=posts, paginacao=paginacao)

# No template:
# {% for p in paginacao.iter_pages() %}

.paginate(page=, per_page=) divide resultados. .items = registos da página. .has_next / .has_prev para navegação. .iter_pages() gera números de página. error_out=False retorna vazio em vez de 404.

Queries e filtros
# Filtros:
Post.query.filter_by(publicado=True).all()
Post.query.filter(Post.views > 100).all()
Post.query.filter(
    Post.titulo.like("%flask%")
).all()

# Múltiplas condições:
from sqlalchemy import and_, or_
Post.query.filter(
    and_(Post.views > 50, Post.publicado == True)
).all()

# Ordenação e limite:
Post.query.order_by(Post.criado.desc()).limit(10).all()

# Contagem e agregação:
Post.query.count()
Post.query.filter_by(publicado=True).count()

# Primeiro ou 404:
post = Post.query.get_or_404(id)

filter_by() para igualdade simples. filter() para expressões complexas (>, like, and_, or_). order_by() + desc() para ordenar. limit() restringe resultados. get_or_404() levanta 404 se não existir.

Eventos e hooks
from sqlalchemy import event

# Antes de inserir:
@event.listens_for(Post, "before_insert")
def antes_inserir(mapper, connection, target):
    target.slug = gerar_slug(target.titulo)

# Depois de atualizar:
@event.listens_for(Post, "after_update")
def depois_update(mapper, connection, target):
    target.atualizado = datetime.utcnow()

# Hybrid properties:
from sqlalchemy.ext.hybrid import hybrid_property

class User(db.Model):
    nome = db.Column(db.String(50))
    apelido = db.Column(db.String(50))

    @hybrid_property
    def nome_completo(self):
        return f"{self.nome} {self.apelido}"

@event.listens_for(Model, "evento") regista hooks. Eventos: before_insert, after_update, before_delete. hybrid_property funciona em Python e em queries SQL. Útil para slugs, timestamps e campos calculados.

Forms e Validação


9 cards
Setup Flask-WTF
pip install flask-wtf email-validator

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email

class LoginForm(FlaskForm):
    email = StringField("Email", validators=[
        DataRequired(message="Email obrigatório"),
        Email(message="Email inválido")
    ])
    senha = PasswordField("Senha", validators=[
        DataRequired()
    ])
    lembrar = BooleanField("Lembrar-me")

# Campos: StringField, TextAreaField,
# SelectField, IntegerField, DateField,
# FileField, BooleanField, RadioField

FlaskForm é a classe base (inclui CSRF). Campos do wtforms com validators. DataRequired() = obrigatório. Email() valida formato (requer email-validator). Cada campo tem .data (valor) e .errors (lista de erros).

Proteção CSRF
# Flask-WTF ativa CSRF automaticamente!
# Requer SECRET_KEY na config.

# No template (obrigatório em todo form):
{{ form.hidden_tag() }}
<!-- ou manualmente: -->
<input type="hidden" name="csrf_token"
       value="{{ csrf_token() }}">

# Para AJAX:
# <meta name="csrf-token" content="{{ csrf_token() }}">
# fetch(url, {
#   headers: {"X-CSRFToken": token}
# })

# Excluir view específica:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)

@app.route("/webhook", methods=["POST"])
@csrf.exempt
def webhook(): ...

CSRFProtect protege todos os POST. {{ form.hidden_tag() }} inclui o token. Para AJAX: enviar X-CSRFToken no header. @csrf.exempt exclui rotas (webhooks, APIs). Sem token → erro 400. Nunca desativar globalmente.

AJAX com forms
# View que retorna JSON:
@app.route("/api/validar", methods=["POST"])
def validar():
    form = RegistoForm()
    if form.validate():
        return {"valido": True}
    return {"valido": False, "erros": form.errors}, 400

# JavaScript:
# const token = document.querySelector(
#     "[name=csrf-token]").content;
#
# fetch("/api/validar", {
#     method: "POST",
#     headers: {
#         "Content-Type": "application/json",
#         "X-CSRFToken": token
#     },
#     body: JSON.stringify({
#         email: "ana@mail.com",
#         csrf_token: token
#     })
# })

form.errors é um dict com erros por campo — serializável para JSON. Enviar X-CSRFToken no header para AJAX. form.validate() sem on_submit para APIs. Retornar 400 com erros para o frontend tratar.

Validadores
from wtforms.validators import (
    DataRequired, Length, Email,
    EqualTo, NumberRange, Regexp,
    Optional, URL
)

class RegistoForm(FlaskForm):
    nome = StringField(validators=[
        Length(min=2, max=50,
               message="2 a 50 caracteres")
    ])
    email = StringField(validators=[
        DataRequired(), Email()
    ])
    senha = PasswordField(validators=[
        Length(min=8, message="Mínimo 8 caracteres"),
        Regexp(r"\d", message="Requer um número")
    ])
    confirmar = PasswordField(validators=[
        EqualTo("senha", message="Senhas não coincidem")
    ])
    idade = IntegerField(validators=[
        Optional(), NumberRange(min=18, max=120)
    ])

Length(min, max) limita tamanho. EqualTo("campo") compara (confirmação de senha). Regexp() valida com regex. NumberRange() para números. Optional() permite vazio. message= personaliza o erro. Validadores executam em ordem.

Validador custom
from wtforms.validators import ValidationError

# Função validadora:
def email_unico(form, field):
    user = User.query.filter_by(
        email=field.data.lower()
    ).first()
    if user:
        raise ValidationError("Email já registado.")

# Usar no form:
class RegistoForm(FlaskForm):
    email = StringField(validators=[
        DataRequired(), Email(), email_unico
    ])

# Método validate_<campo> (automático):
class RegistoForm(FlaskForm):
    username = StringField()

    def validate_username(self, field):
        if len(field.data) < 3:
            raise ValidationError("Mínimo 3 caracteres.")
        if " " in field.data:
            raise ValidationError("Sem espaços.")

Validador custom: função que recebe (form, field) e levanta ValidationError. Método validate_<campo> é chamado automaticamente. Ambos adicionam a field.errors. Ideal para validações que dependem da BD (unicidade).

Processar na view
@app.route("/registo", methods=["GET", "POST"])
def registo():
    form = RegistoForm()

    if form.validate_on_submit():
        # Dados válidos:
        user = User(
            nome=form.nome.data,
            email=form.email.data
        )
        user.set_password(form.senha.data)
        db.session.add(user)
        db.session.commit()

        flash("Conta criada!", "sucesso")
        return redirect(url_for("login"))

    # GET ou validação falhou:
    return render_template("registo.html", form=form)

# validate_on_submit() = POST + válido

form.validate_on_submit() verifica se é POST E dados válidos. form.campo.data acede ao valor limpo. Se inválido, re-renderiza com erros. Padrão: GET mostra form, POST processa. flash() + redirect() após sucesso (PRG pattern).

File upload com form
from flask_wtf.file import FileField, FileAllowed, FileRequired

class UploadForm(FlaskForm):
    foto = FileField("Foto de perfil", validators=[
        FileRequired(message="Selecione um ficheiro"),
        FileAllowed(["jpg", "png", "webp"],
                    message="Apenas imagens!")
    ])

# Na view:
@app.route("/upload", methods=["POST"])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        f = form.foto.data
        nome = secure_filename(f.filename)
        f.save(f"static/uploads/{nome}")
        flash("Upload feito!")
        return redirect(url_for("perfil"))
    return render_template("upload.html", form=form)

# HTML: <form enctype="multipart/form-data">

FileField para uploads. FileAllowed(["ext"]) valida extensão. FileRequired() torna obrigatório. form.campo.data é o objeto ficheiro. Formulário precisa de enctype="multipart/form-data". Combinar com secure_filename().

Renderizar no template
<!-- registo.html -->
<form method="POST" novalidate>
    {{ form.hidden_tag() }}

    <div class="field">
        {{ form.nome.label }}
        {{ form.nome(class="input", placeholder="Nome") }}
        {% for erro in form.nome.errors %}
            <span class="erro">{{ erro }}</span>
        {% endfor %}
    </div>

    <div class="field">
        {{ form.email.label }}
        {{ form.email(class="input") }}
        {% for erro in form.email.errors %}
            <span class="erro">{{ erro }}</span>
        {% endfor %}
    </div>

    {{ form.submit(class="btn") }}
</form>

{{ form.hidden_tag() }} renderiza o token CSRF (obrigatório!). {{ form.campo() }} gera o input HTML. class= adiciona classes CSS. form.campo.errors lista erros de validação. form.campo.label gera o <label>.

Form com dados iniciais
# Editar registo existente:
@app.route("/post/<int:id>/editar", methods=["GET", "POST"])
def editar(id):
    post = Post.query.get_or_404(id)
    form = PostForm(obj=post)  # preencher!

    if form.validate_on_submit():
        form.populate_obj(post)  # atualizar!
        db.session.commit()
        flash("Post atualizado!")
        return redirect(url_for("post", id=id))

    return render_template("editar.html",
                           form=form, post=post)

# populate_obj copia form → objeto
# obj=post copia objeto → form (GET)

# Ou manualmente:
form = PostForm(titulo=post.titulo,
                corpo=post.corpo)

PostForm(obj=post) preenche o form com dados do objeto. form.populate_obj(post) faz o inverso (form → objeto). Ideal para edição: GET mostra dados atuais, POST atualiza. Alternativa: passar campos manualmente no construtor.

Blueprints


9 cards
Criar blueprint
# app/auth/routes.py
from flask import Blueprint, render_template

bp = Blueprint(
    "auth",              # nome único
    __name__,            # módulo
    url_prefix="/auth",  # prefixo URL
    template_folder="templates",
    static_folder="static"
)

@bp.route("/login")
def login():
    return render_template("auth/login.html")

@bp.route("/registo")
def registo():
    return render_template("auth/registo.html")

# URLs finais: /auth/login, /auth/registo

Blueprint(nome, __name__) cria um módulo independente. url_prefix prefixa todas as rotas. template_folder e static_folder são opcionais. Cada blueprint é uma mini-app. Ideal para separar auth, blog, admin, API.

Error handlers por BP
# Erro específico do blueprint:
@bp.app_errorhandler(404)
def bp_404(e):
    return render_template("auth/404.html"), 404

# Before request só neste BP:
@bp.before_app_request
def verificar_auth():
    if request.endpoint and \
       request.endpoint.startswith("admin."):
        if not current_user.is_admin:
            abort(403)

# Context processor do BP:
@bp.context_processor
def inject_auth():
    return {"auth_version": "2.0"}

@bp.app_errorhandler() regista handlers globais. @bp.before_app_request executa antes de todos os requests (não só do BP). @bp.context_processor injeta variáveis nos templates. Útil para middleware específico de um módulo.

Testar blueprints
import pytest
from app import create_app

@pytest.fixture
def app():
    app = create_app("testing")
    app.config["TESTING"] = True
    return app

@pytest.fixture
def client(app):
    return app.test_client()

# Testar BP auth:
def test_login_page(client):
    r = client.get("/auth/login")
    assert r.status_code == 200
    assert b"Login" in r.data

def test_login_post(client):
    r = client.post("/auth/login", data={
        "email": "ana@mail.com",
        "senha": "123456"
    }, follow_redirects=True)
    assert b"Dashboard" in r.data

Testar BPs como rotas normais com test_client(). create_app("testing") usa config de teste. follow_redirects=True segue redirects. r.data é bytes — usar b"texto". Fixtures do pytest para setup. Um ficheiro de teste por BP.

Registar blueprints
# app/__init__.py
from flask import Flask

def create_app():
    app = Flask(__name__)

    from .auth.routes import bp as auth_bp
    from .blog.routes import bp as blog_bp
    from .api.routes import bp as api_bp

    app.register_blueprint(auth_bp)
    app.register_blueprint(blog_bp)
    app.register_blueprint(api_bp, url_prefix="/api/v1")

    return app

# url_prefix no registo sobrescreve
# o prefixo definido no blueprint

app.register_blueprint(bp) adiciona à app. Importar dentro do factory evita imports circulares. url_prefix no registo sobrescreve o do blueprint. Ordem de registo não importa. Cada BP pode ter o seu próprio prefixo.

API versioning
# api/v1/routes.py
bp_v1 = Blueprint("api_v1", __name__)

@bp_v1.route("/users")
def users():
    return jsonify({"versao": 1, "users": []})

# api/v2/routes.py
bp_v2 = Blueprint("api_v2", __name__)

@bp_v2.route("/users")
def users():
    return jsonify({"versao": 2, "data": []})

# Registar:
app.register_blueprint(bp_v1, url_prefix="/api/v1")
app.register_blueprint(bp_v2, url_prefix="/api/v2")

# /api/v1/users → versão 1
# /api/v2/users → versão 2

Versionar com blueprints: um BP por versão. url_prefix="/api/v1" separa. Clientes antigos continuam a funcionar. Nova versão = novo BP sem quebrar existente. Alternativa: header Accept-Version. Padrão para APIs públicas.

Estrutura modular
app/
├── __init__.py          # create_app()
├── extensions.py        # db, migrate, login
├── auth/
│   ├── __init__.py
│   ├── routes.py        # bp auth
│   ├── forms.py         # LoginForm
│   └── templates/auth/
├── blog/
│   ├── __init__.py
│   ├── routes.py        # bp blog
│   ├── models.py        # Post, Comment
│   └── templates/blog/
├── api/
│   ├── __init__.py
│   └── routes.py        # bp api
└── templates/           # base.html global

Cada funcionalidade num pacote próprio. extensions.py centraliza db, migrate, login (evita imports circulares). Templates do BP em templates/nome_bp/. models.py por módulo. Escalável para equipas grandes.

Templates por blueprint
# Blueprint com templates próprios:
bp = Blueprint("blog", __name__,
    template_folder="templates",
    static_folder="static",
    static_url_path="/blog/static"
)

# Estrutura:
# app/blog/
#   templates/blog/
#     index.html
#     post.html
#   static/
#     blog.css

# No route do BP:
@bp.route("/")
def index():
    # Procura em blog/templates/blog/index.html
    return render_template("blog/index.html")

# Herdar do base global:
# {% extends "base.html" %}

template_folder define pasta de templates do BP. Usar subpasta com nome do BP evita conflitos. static_folder + static_url_path para assets próprios. Templates do BP podem herdar do base.html global. Prefixar nomes para clareza.

url_for com blueprints
# Formato: "nome_bp.funcao"
url_for("auth.login")           # /auth/login
url_for("blog.post", id=5)      # /blog/post/5
url_for("api.users")            # /api/v1/users

# No template:
# <a href="{{ url_for('auth.login') }}">Login</a>
# <a href="{{ url_for('blog.post', id=p.id) }}">

# Redirecionar entre BPs:
return redirect(url_for("auth.login"))

# Verificar endpoint atual:
request.endpoint  # "auth.login"
request.blueprint # "auth"

url_for("bp.funcao") referencia rotas de blueprints. O prefixo é o nome do BP (1º arg do Blueprint). request.endpoint mostra o endpoint atual. request.blueprint mostra o BP. Sempre usar url_for — nunca hardcode URLs.

Nested blueprints
# Flask 2.0+ suporta BPs aninhados:
parent = Blueprint("admin", __name__,
                   url_prefix="/admin")
child = Blueprint("users", __name__,
                  url_prefix="/users")

@child.route("/")
def listar():
    return "Lista de users"

# Aninhar:
parent.register_blueprint(child)
app.register_blueprint(parent)

# URL final: /admin/users/

# Útil para:
# /admin/users/
# /admin/posts/
# /admin/settings/
# Cada sub-módulo é um BP filho

Flask 2.0+ permite parent.register_blueprint(child). Prefixos acumulam: /admin + /users = /admin/users. Ideal para painéis admin com sub-módulos. Cada nível é independente e testável. Evitar mais de 2 níveis de nesting.

Extensões e Deploy


9 cards
Flask-Login (autenticação)
pip install flask-login

from flask_login import (
    LoginManager, UserMixin,
    login_user, logout_user,
    login_required, current_user
)

login = LoginManager(app)
login.login_view = "auth.login"  # redirect

class User(UserMixin, db.Model):
    # UserMixin adiciona: is_authenticated,
    # is_active, is_anonymous, get_id()
    pass

@login.user_loader
def load_user(id):
    return db.session.get(User, int(id))

# Proteger rota:
@app.route("/dashboard")
@login_required
def dashboard():
    return f"Olá {current_user.nome}"

flask-login gere sessões de utilizador. UserMixin adiciona métodos obrigatórios. @login_required protege rotas. current_user é o utilizador atual. login_user() / logout_user() gerem a sessão. login_view define redirect se não autenticado.

Deploy com Gunicorn
pip install gunicorn

# Executar:
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"

# Opções:
gunicorn \
    --workers 4 \
    --bind 0.0.0.0:8000 \
    --timeout 120 \
    --access-logfile - \
    --error-logfile - \
    "app:create_app()"

# Workers = (2 × CPU cores) + 1

# Nginx como reverse proxy:
# proxy_pass http://127.0.0.1:8000;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;

# systemd para gerir processo

gunicorn é o servidor WSGI de produção. -w 4 = 4 workers (processos). "app:create_app()" chama o factory. Nunca usar flask run em produção. Nginx na frente como reverse proxy. systemd para auto-restart.

Segurança e boas práticas
# 1. Headers de segurança:
from flask_talisman import Talisman
Talisman(app, force_https=True)

# 2. Rate limiting:
from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)

@app.route("/api/login")
@limiter.limit("5/minute")
def login(): ...

# 3. Nunca debug em produção:
app.config["DEBUG"] = False

# 4. Validar TODO input:
# request.json pode ser None!
data = request.get_json(silent=True) or {}

# 5. Usar parâmetros em SQL:
db.session.execute(
    text("SELECT * FROM users WHERE id = :id"),
    {"id": user_id}
)

# 6. HTTPS obrigatório em produção

flask-talisman força HTTPS e headers seguros. flask-limiter limita requests (anti brute-force). Nunca DEBUG=True em produção. Validar input (get_json(silent=True)). Parâmetros em SQL (anti injection). HTTPS obrigatório. SECRET_KEY forte e em env vars.

Flask-RESTful (API)
pip install flask-restful

from flask_restful import Resource, Api, reqparse

api = Api(app)

class UserResource(Resource):
    def get(self, user_id):
        user = User.query.get_or_404(user_id)
        return {"id": user.id, "nome": user.nome}

    def put(self, user_id):
        parser = reqparse.RequestParser()
        parser.add_argument("nome", required=True)
        args = parser.parse_args()
        user.nome = args["nome"]
        db.session.commit()
        return {"ok": True}

    def delete(self, user_id):
        return "", 204

api.add_resource(UserResource, "/api/user/<int:user_id>")

Resource define endpoints com métodos HTTP. Api(app) regista. reqparse valida argumentos. add_resource() mapeia URL. Retornar dict = JSON automático. Alternativa moderna: flask-smorest ou flask-restx com OpenAPI.

Docker
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", \
     "app:create_app()"]

# docker-compose.yml
# services:
#   web:
#     build: .
#     ports: ["8000:8000"]
#     environment:
#       - SECRET_KEY=super
#       - DATABASE_URL=postgresql://...
#   db:
#     image: postgres:16
#     volumes: [pgdata:/var/lib/postgresql/data]

# docker compose up --build

python:3.12-slim como base (leve). Instalar dependências antes do COPY (cache Docker). gunicorn como CMD. docker-compose orquestra web + BD. Variáveis de ambiente para config. --build reconstrói a imagem.

Context processors
# Variáveis globais em todos os templates:
@app.context_processor
def inject_globals():
    return {
        "app_name": "Minha App",
        "ano_atual": datetime.now().year,
        "menu_items": ["Início", "Blog", "Contacto"]
    }

# Em qualquer template:
# {{ app_name }} - {{ ano_atual }}

# Com blueprint:
@bp.context_processor
def inject_bp():
    return {"bp_version": "1.0"}

# Utility functions:
@app.context_processor
def inject_utils():
    return {"formatar_data": lambda d: d.strftime("%d/%m/%Y")}

@app.context_processor injeta variáveis em todos os templates. Retornar dict. Executa em cada request (manter leve). Ideal para: nome da app, ano, menu, user atual. Alternativa: app.jinja_env.globals para funções.

Logging
import logging
from logging.handlers import RotatingFileHandler

def setup_logging(app):
    handler = RotatingFileHandler(
        "logs/app.log",
        maxBytes=5_000_000,  # 5MB
        backupCount=5
    )
    handler.setFormatter(logging.Formatter(
        "%(asctime)s %(levelname)s: %(message)s"
    ))
    handler.setLevel(logging.INFO)

    app.logger.addHandler(handler)
    app.logger.setLevel(logging.INFO)

# Usar:
app.logger.info("Servidor iniciado")
app.logger.warning("Cache quase cheio")
app.logger.error(f"Erro: {e}")

# Em produção: log para stdout (Docker)

app.logger é o logger do Flask. RotatingFileHandler limita tamanho dos logs. logging.Formatter define o formato. Níveis: DEBUG, INFO, WARNING, ERROR. Em Docker: log para stdout. Nunca logar dados sensíveis.

Testing com pytest
# conftest.py
import pytest
from app import create_app, db

@pytest.fixture
def app():
    app = create_app("testing")
    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()

# test_routes.py
def test_home(client):
    r = client.get("/")
    assert r.status_code == 200

def test_create_post(client, auth_headers):
    r = client.post("/posts", json={
        "titulo": "Teste"
    }, headers=auth_headers)
    assert r.status_code == 201

# Executar: pytest -v

conftest.py define fixtures partilhadas. create_app("testing") usa BD de teste. db.create_all() / drop_all() por teste. test_client() simula requests HTTP. pytest -v executa. Config testing: SQLALCHEMY_DATABASE_URI = "sqlite://" (memória).

Flask-Mail
pip install flask-mail

from flask_mail import Mail, Message

mail = Mail(app)

app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_PORT"] = 587
app.config["MAIL_USE_TLS"] = True
app.config["MAIL_USERNAME"] = "app@gmail.com"
app.config["MAIL_PASSWORD"] = "senha-app"

def enviar_email(destinatario, assunto, corpo):
    msg = Message(
        subject=assunto,
        recipients=[destinatario],
        body=corpo,
        sender="noreply@app.com"
    )
    mail.send(msg)

# HTML:
msg.html = "<h1>Olá!</h1>"

flask-mail envia emails via SMTP. Configurar servidor, porta, credenciais. Message() cria o email. mail.send() envia (síncrono). msg.html para conteúdo HTML. Em produção: usar fila (Celery) para não bloquear. Gmail requer senha de app.