Partilhar: WhatsApp
aulify · Sebenta
UC · Unidade de Competência · UC02834

Programação web

Sebenta · Backend, Flask/FastAPI, REST, BD
50h · 4.5 pontos crédito Curso: T. Desenv. Software ↗ Referencial oficial SNQ
Índice

Apresentação

UC02834 (50h · 4,5 pts) é onde tudo se junta: o frontend da UC02833, o Python das UCs de programação, o SQL/ORM das UCs de BD e a segurança da UC02836. Constrói-se um servidor web completo: rotas, templates, formulários, API REST, BD, autenticação e deploy.

Como funciona a web

Cliente e servidor

Browser ── GET /produtos ──► Servidor ── query ──► BD
Browser ◄── 200 + HTML/JSON ─ Servidor ◄── dados ─ BD

O servidor nunca confia no cliente: qualquer validação de segurança é feita no servidor.

HTTP recapitulado

Frameworks

Porquê um framework

Escrever um servidor HTTP do zero é complexo (parsing, routing, segurança). Frameworks tratam disso:

Esta sebenta usa Flask como base e mostra FastAPI como alternativa moderna.

Setup

python -m venv venv
source venv/bin/activate
pip install flask
# app.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "<h1>Olá, Aulify!</h1>"

if __name__ == "__main__":
    app.run(debug=True)
flask run
# http://localhost:5000

debug=True só em desenvolvimento — recarrega ao guardar e mostra erros. Nunca em produção.

Rotas

Rotas básicas

@app.route("/")
def home():
    return "Início"

@app.route("/sobre")
def sobre():
    return "Sobre nós"

Parâmetros de rota

@app.route("/produto/<int:id>")
def produto(id):
    return f"Produto {id}"

@app.route("/utilizador/<username>")
def perfil(username):
    return f"Perfil de {username}"

Conversores: <int:x>, <float:x>, <string:x>, <path:x>.

Query string

from flask import request

@app.route("/pesquisa")
def pesquisa():
    termo = request.args.get("q", "")
    pagina = request.args.get("page", 1, type=int)
    return f"Pesquisa '{termo}', página {pagina}"
# /pesquisa?q=python&page=2

Métodos HTTP

@app.route("/contacto", methods=["GET", "POST"])
def contacto():
    if request.method == "POST":
        nome = request.form["nome"]
        email = request.form["email"]
        # processar...
        return redirect(url_for("obrigado"))
    return render_template("contacto.html")

Templates Jinja2

Renderizar

from flask import render_template

@app.route("/produtos")
def produtos():
    lista = [
        {"nome": "Caneta", "preco": 1.5},
        {"nome": "Caderno", "preco": 3.2},
    ]
    return render_template("produtos.html", produtos=lista, titulo="Loja")

Sintaxe Jinja2

<!-- templates/produtos.html -->
<h1>{{ titulo }}</h1>

<ul>
{% for p in produtos %}
  <li>{{ p.nome }} — {{ "%.2f"|format(p.preco) }}€</li>
{% else %}
  <li>Sem produtos</li>
{% endfor %}
</ul>

{% if produtos|length > 5 %}
  <p>Muitos produtos!</p>
{% endif %}

Cuidado com |safe — desliga o escape, abre porta a XSS. Só usar com HTML confiável.

Herança de templates

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="pt">
<head>
  <meta charset="UTF-8">
  <title>{% block titulo %}Aulify{% endblock %}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  <header>...</header>
  <main>{% block conteudo %}{% endblock %}</main>
  <footer>...</footer>
</body>
</html>
<!-- templates/produtos.html -->
{% extends "base.html" %}
{% block titulo %}Produtos — Aulify{% endblock %}
{% block conteudo %}
  <h1>Os nossos produtos</h1>
{% endblock %}

DRY: o layout comum existe num só ficheiro.

Ficheiros estáticos

<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<img src="{{ url_for('static', filename='img/logo.png') }}" alt="Logo">

Estrutura: static/css/, static/js/, static/img/.

Formulários

Receber dados

@app.route("/registar", methods=["GET", "POST"])
def registar():
    if request.method == "POST":
        nome = request.form.get("nome", "").strip()
        email = request.form.get("email", "").strip()
        erros = []
        if len(nome) < 2:
            erros.append("Nome demasiado curto")
        if "@" not in email:
            erros.append("Email inválido")
        if erros:
            return render_template("registar.html", erros=erros)
        # guardar utilizador...
        flash("Conta criada!")
        return redirect(url_for("login"))
    return render_template("registar.html")

CSRF

Formulários POST devem ter token CSRF. Com Flask-WTF:

from flask_wtf import CSRFProtect
csrf = CSRFProtect(app)
<form method="post">
  {{ csrf_token() }}
  ...
</form>

API REST

Princípios

Operação Método + rota Código sucesso
Listar GET /produtos 200
Obter um GET /produtos/5 200 (ou 404)
Criar POST /produtos 201
Substituir PUT /produtos/5 200
Modificar PATCH /produtos/5 200
Apagar DELETE /produtos/5 204

API com Flask

from flask import jsonify, request

produtos = []

@app.route("/api/produtos", methods=["GET"])
def listar():
    return jsonify(produtos)

@app.route("/api/produtos/<int:id>", methods=["GET"])
def obter(id):
    p = next((x for x in produtos if x["id"] == id), None)
    if p is None:
        return jsonify({"erro": "Não encontrado"}), 404
    return jsonify(p)

@app.route("/api/produtos", methods=["POST"])
def criar():
    dados = request.get_json()
    if not dados or "nome" not in dados:
        return jsonify({"erro": "nome obrigatório"}), 400
    novo = {"id": len(produtos) + 1, "nome": dados["nome"]}
    produtos.append(novo)
    return jsonify(novo), 201

@app.route("/api/produtos/<int:id>", methods=["DELETE"])
def apagar(id):
    global produtos
    produtos = [x for x in produtos if x["id"] != id]
    return "", 204

FastAPI

Alternativa moderna com validação automática:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class Produto(BaseModel):
    nome: str
    preco: float

produtos: list[Produto] = []

@app.get("/produtos")
def listar() -> list[Produto]:
    return produtos

@app.post("/produtos", status_code=201)
def criar(p: Produto) -> Produto:
    produtos.append(p)
    return p

@app.get("/produtos/{id}")
def obter(id: int) -> Produto:
    if id >= len(produtos):
        raise HTTPException(404, "Não encontrado")
    return produtos[id]

Vantagens: validação Pydantic automática (400 se input errado), documentação interactiva em /docs, type hints, suporte assíncrono.

Base de dados

Flask-SQLAlchemy

from flask_sqlalchemy import SQLAlchemy

app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db = SQLAlchemy(app)

class Produto(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    nome = db.Column(db.String(150), nullable=False)
    preco = db.Column(db.Float, nullable=False)

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

CRUD

# Criar
p = Produto(nome="Caneta", preco=1.5)
db.session.add(p)
db.session.commit()

# Ler
todos = Produto.query.all()
um = Produto.query.get(1)
filtrados = Produto.query.filter(Produto.preco < 5).all()

# Actualizar
p = Produto.query.get(1)
p.preco = 2.0
db.session.commit()

# Apagar
db.session.delete(p)
db.session.commit()

Rota com BD

@app.route("/produtos")
def listar():
    produtos = Produto.query.order_by(Produto.nome).all()
    return render_template("produtos.html", produtos=produtos)

@app.route("/produtos/novo", methods=["POST"])
def novo():
    p = Produto(
        nome=request.form["nome"],
        preco=float(request.form["preco"])
    )
    db.session.add(p)
    db.session.commit()
    return redirect(url_for("listar"))

Autenticação

Hash de senhas

from werkzeug.security import generate_password_hash, check_password_hash

# Registo
pwd_hash = generate_password_hash(senha)   # nunca guardar texto plano

# Login
if check_password_hash(user.pwd_hash, senha_introduzida):
    # OK
    pass

Sessões com Flask-Login

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

login_manager = LoginManager(app)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(150), unique=True)
    pwd_hash = db.Column(db.String(255))

@login_manager.user_loader
def load_user(uid):
    return User.query.get(int(uid))

@app.route("/login", methods=["POST"])
def login():
    user = User.query.filter_by(email=request.form["email"]).first()
    if user and check_password_hash(user.pwd_hash, request.form["senha"]):
        login_user(user)
        return redirect(url_for("painel"))
    flash("Credenciais inválidas")
    return redirect(url_for("login"))

@app.route("/painel")
@login_required
def painel():
    return f"Olá {current_user.email}"

@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for("home"))

Segurança (recap UC02836)

  1. Senhas — sempre hash (werkzeug/bcrypt).
  2. SQL — ORM ou prepared statements; nunca concatenar.
  3. XSS — Jinja2 auto-escapa; cuidado com |safe.
  4. CSRF — token em formulários (Flask-WTF).
  5. HTTPS — obrigatório em produção.
  6. Validação no servidor, sempre.
  7. Secrets em variáveis de ambiente, nunca no código.
  8. CookiesSecure, HttpOnly, SameSite.
  9. Debug=False em produção.
  10. Dependências auditadas (pip-audit).

Estrutura e deploy

Estrutura típica

app/
├── app.py
├── models.py
├── config.py
├── routes/
│   ├── auth.py
│   └── api.py
├── templates/
│   ├── base.html
│   └── ...
├── static/
│   ├── css/
│   └── js/
├── requirements.txt
├── .env
└── .gitignore

Blueprints (organizar)

# routes/api.py
from flask import Blueprint, jsonify

api = Blueprint("api", __name__, url_prefix="/api")

@api.route("/produtos")
def listar():
    return jsonify([...])
# app.py
from routes.api import api
app.register_blueprint(api)

Deploy

Em produção nunca flask run. Usar servidor WSGI:

pip install gunicorn
gunicorn app:app --workers 4 --bind 0.0.0.0:8000

Plataformas: - Render / Railway / Fly.io — simples, plano grátis, Git push deploy. - VPS + Nginx + Gunicorn + systemd — controlo total. - Vercel / Serverless — bom para FastAPI/Next.js.

Checklist de produção: - DEBUG = False. - Secrets em variáveis de ambiente. - HTTPS (Let's Encrypt). - BD gerida (não SQLite em produção séria — usar PostgreSQL). - Logs e monitorização. - Backups da BD.

Apêndices

A · Cheatsheet Flask

@app.route("/x", methods=["GET","POST"])
request.args.get("q")          # query string
request.form["campo"]          # form POST
request.get_json()             # body JSON
render_template("x.html", v=1) # template
jsonify(data), 200             # JSON
redirect(url_for("rota"))      # redirect
flash("msg")                   # mensagem flash
@login_required                # proteger rota

B · Glossário

API REST. Interface HTTP com recursos e métodos. Backend. Lógica do lado do servidor. Blueprint. Módulo de rotas Flask. Endpoint. URL que a API expõe. Jinja2. Motor de templates do Flask. ORM. Object-Relational Mapping. WSGI. Interface servidor-aplicação Python (Gunicorn).

C · Recursos