Programação web
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
- Cliente (browser) — pede páginas, executa HTML/CSS/JS, mostra ao utilizador.
- Servidor (backend) — recebe pedidos HTTP, processa lógica, acede a BD, devolve resposta.
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
- Métodos: GET (ler), POST (criar/enviar), PUT/PATCH (alterar), DELETE (apagar).
- Códigos: 2xx sucesso, 3xx redirect, 4xx erro do cliente, 5xx erro do servidor.
- Headers: Content-Type, Authorization, Cookie.
- Body: dados (form-encoded, JSON, ficheiros).
Frameworks
Porquê um framework
Escrever um servidor HTTP do zero é complexo (parsing, routing, segurança). Frameworks tratam disso:
- Flask — micro-framework Python, simples, flexível, ótimo para aprender.
- FastAPI — moderno, assíncrono, validação automática, docs OpenAPI.
- Django — "baterias incluídas" (ORM, admin, auth), mais pesado.
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 %}
{{ variavel }}— output (escapado automaticamente, anti-XSS).{% logica %}— controlo de fluxo.{{ var|filtro }}— filtros (upper,length,format,safe...).
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
- Recursos como substantivos no plural:
/produtos,/utilizadores. - Métodos HTTP definem a acção (não
/getProdutos). - Respostas em JSON.
- Códigos de estado significativos.
| 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)
- Senhas — sempre hash (werkzeug/bcrypt).
- SQL — ORM ou prepared statements; nunca concatenar.
- XSS — Jinja2 auto-escapa; cuidado com
|safe. - CSRF — token em formulários (Flask-WTF).
- HTTPS — obrigatório em produção.
- Validação no servidor, sempre.
- Secrets em variáveis de ambiente, nunca no código.
- Cookies —
Secure,HttpOnly,SameSite. - Debug=False em produção.
- 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
- Flask docs · flask.palletsprojects.com
- FastAPI docs · fastapi.tiangolo.com
- Miguel Grinberg · Flask Mega-Tutorial — referência clássica.
- MDN · Server-side · developer.mozilla.org
- Real Python · Flask · realpython.com