Laboratorio. Trabajando con Flask
| Sitio: | Campus virtual DAW - damiansu |
| Curso: | Programación en Python |
| Libro: | Laboratorio. Trabajando con Flask |
| Imprimido por: | Invitado |
| Día: | jueves, 22 de enero de 2026, 04:41 |
1. Introducción
El objetivo es:
-
Montar Flask desde cero
-
Crear rutas
-
Usar parámetros
-
Devolver HTML y JSON
-
Entender cómo funciona una API
-
Ver la simplicidad y potencia del framework
2. Preparación del entorno
Crear carpeta del proyecto
mkdir mi_primer_flask
cd mi_primer_flask
Crear entorno virtual
Windows:
python -m venv venv
.\venv\Scripts\Activate1.ps1
Mac:
python -m venv venv
source venv/bin/activate
Instalar Flask
pip install flask
Collecting flask
Downloading flask-3.1.2-py3-none-any.whl.metadata (3.2 kB)
Collecting blinker>=1.9.0 (from flask)
Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting click>=8.1.3 (from flask)
Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
Collecting itsdangerous>=2.2.0 (from flask)
Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting jinja2>=3.1.2 (from flask)
Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting markupsafe>=2.1.1 (from flask)
Downloading markupsafe-3.0.3-cp313-cp313-win_amd64.whl.metadata (2.8 kB)
Collecting werkzeug>=3.1.0 (from flask)
Downloading werkzeug-3.1.4-py3-none-any.whl.metadata (4.0 kB)
Collecting colorama (from click>=8.1.3->flask)
Downloading colorama-0.4.6-py2.py3-none-any.whl.metadata (17 kB)
Downloading flask-3.1.2-py3-none-any.whl (103 kB)
Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
Downloading click-8.3.1-py3-none-any.whl (108 kB)
Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
Downloading markupsafe-3.0.3-cp313-cp313-win_amd64.whl (15 kB)
Downloading werkzeug-3.1.4-py3-none-any.whl (224 kB)
Downloading colorama-0.4.6-py2.py3-none-any.whl (25 kB)
, flask
Successfully installed blinker-1.9.0 click-8.3.1 colorama-0.4.6 flask-3.1.2 itsdangerous-2.2.0 jinja2-3.1.6 markupsafe-3.0.3 werkzeug-3.1.43. Primer “Hola Mundo” en Flask
Crea un archivo app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def inicio():
return "¡Hola Flask! Mi primera aplicación."
if __name__ == "__main__":
app.run(debug=True)
from flask import Flask
app = Flask(__name__)
@app.route("/")
def inicio():
return "¡Hola Flask! Mi primera aplicación."
if __name__ == "__main__":
app.run(debug=True)
Ejecutar:
python app.py
Abrir navegador en:
python app.py
Acabamos de convertir Python en un servidor web en menos de 1 minuto.
Flask es flexible porque no te obliga a nada: tú decides la estructura.
* Serving Flask app 'app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 829-682-556
127.0.0.1 - - [11/Dec/2025 13:23:16] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [11/Dec/2025 13:23:17] "GET /favicon.ico HTTP/1.1" 404 -4. Varias rutas (y URLs con parámetros)
Amplía app.py:
@app.route("/saludo")def saludo(): return "Hola a todos desde Flask!"
@app.route("/usuario/<nombre>")def usuario(nombre): return f"Bienvenido, {nombre}!"
@app.route("/suma/<int:a>/<int:b>")def suma(a, b): return f"La suma es {a + b}"
@app.route("/saludo")
def saludo():
return "Hola a todos desde Flask!"
@app.route("/usuario/<nombre>")
def usuario(nombre):
return f"Bienvenido, {nombre}!"
@app.route("/suma/<int:a>/<int:b>")
def suma(a, b):
return f"La suma es {a + b}"
Prueba:
4.1. Código completo
from flask import Flask
app = Flask(__name__)
@app.route("/")
def inicio():
return "¡Hola Flask! Mi primera aplicación."
@app.route("/saludo")
def saludo():
return "Hola a todos desde Flask!"
@app.route("/usuario/<nombre>")
def usuario(nombre):
return f"Bienvenido, {nombre}!"
@app.route("/suma/<int:a>/<int:b>")
def suma(a, b):
return f"La suma es {a + b}"
if __name__ == "__main__":
app.run(debug=True)
5. Devolver JSON (mini API sin BD)
Ahora vamos a añadir
from flask import jsonify
productos = [
{"id": 1, "nombre": "Monitor", "precio": 149.99},
{"id": 2, "nombre": "Teclado", "precio": 29.99},
]
@app.route("/api/productos")
def obtener_productos():
return jsonify({"productos": productos})
@app.route("/api/productos/<int:id>")
def obtener_producto(id):
producto = next((p for p in productos if p["id"] == id), None)
if producto:
return jsonify(producto)
return jsonify({"error": "Producto no encontrado"}), 404
Probar:
Hemos construido una primera API real sin base de datos.
5.1. Código completo
from flask import Flask
from flask import jsonify
app = Flask(__name__)
@app.route("/")
def inicio():
return "¡Hola Flask! Mi primera aplicación."
@app.route("/saludo")
def saludo():
return "Hola a todos desde Flask!"
@app.route("/usuario/<nombre>")
def usuario(nombre):
return f"Bienvenido, {nombre}!"
@app.route("/suma/<int:a>/<int:b>")
def suma(a, b):
return f"La suma es {a + b}"
productos = [
{"id": 1, "nombre": "Monitor", "precio": 149.99},
{"id": 2, "nombre": "Teclado", "precio": 29.99},
]
@app.route("/api/productos")
def obtener_productos():
return jsonify({"productos": productos})
@app.route("/api/productos/<int:id>")
def obtener_producto(id):
producto = next((p for p in productos if p["id"] == id), None)
if producto:
return jsonify(producto)
return jsonify({"error": "Producto no encontrado"}), 404
if __name__ == "__main__":
app.run(debug=True)
6. Introducción a plantillas HTML
Crea una carpeta:
mkdir templates
Dentro crea inicio.html:
<!DOCTYPE html>
<html>
<head>
<title>Inicio Flask</title>
</head>
<body>
<h1>Bienvenidos a mi primera web con Flask</h1>
<p>Esto viene de una plantilla HTML.</p>
</body>
</html>
Actualiza app.py:
from flask import render_template
@app.route("/web")
def web():
return render_template("inicio.html")
Accedemos a la plantilla:
7. Ejercicios finales
7.1. /hora
Crear una ruta /hora que devuelva la hora actual.
En app.py:
from datetime import datetime
@app.route("/hora")
def hora():
ahora = datetime.now().strftime("%H:%M:%S")
return f"La hora actual es: {ahora}"
En navegador:
7.2. /cuadrado/
Crear una ruta /cuadrado/<numero> que muestre el cuadrado del número
Vamos a Trabajar con parámetros numéricos en la URL.
@app.route("/cuadrado/<int:numero>")
def cuadrado(numero):
resultado = numero * numero
return f"El cuadrado de {numero} es {resultado}"
En el navegador
http://127.0.0.1:5000/cuadrado/77.3. /api/productos
Añadir un producto nuevo a la lista y mostrarlo en /api/productos
Vamos a manipular listas y ver cómo devolver JSON desde Flask.
Estado previo:
productos = [
{"id": 1, "nombre": "Monitor", "precio": 149.99},
{"id": 2, "nombre": "Teclado", "precio": 29.99},
]
Agregar un nuevo producto
productos.append({
"id": 3,
"nombre": "Ratón",
"precio": 19.99
})
Resultado final del listado
@app.route("/api/productos")
def obtener_productos():
return jsonify({"productos": productos})
En el navegador
http://127.0.0.1:5000/api/productos7.4. Plantilla saludo
Crear una plantilla saludo.html que muestre un mensaje recibido por URL
Vamos a trabajar con plantillas HTML y el motor Jinja2.
Paso 1: Crear archivo templates/saludo.html
<!DOCTYPE html>
<html>
<head>
<title>Saludo</title>
</head>
<body>
<h1>Hola, {{ nombre }}!</h1>
<p>Bienvenido a mi página hecha con Flask.</p>
</body>
</html>
Paso 2: Crear la ruta en app.py
from flask import render_template
@app.route("/saludo/<nombre>")
def saludito(nombre):
return render_template("saludo.html", nombre=nombre)
Probamos
http://127.0.0.1:5000/saludo/Ana
http://127.0.0.1:5000/saludo/Pepe8. API Restful
Empezamos con la API RESTful
8.1. Conexión con la base de datos
Instalamos dependencias
pip install pymysql
pip install cryptography
pip install sqlalchemy
Importamos estas líbrerías:
from sqlalchemy import create_engine, MetaData, Table, select
from sqlalchemy.engine import URL
Introducimos este código de conexión adaptado:
DATABASE_URL = URL.create(
drivername="mysql+pymysql",
username="root",
password="ChuckNorris2025",
host="localhost",
port=3307,
database="tienda_virtual",
)
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
)
metadata = MetaData()
productos_t = Table(
"productos_producto",
metadata,
autoload_with=engine
)
categorias_t = Table(
"productos_categoria",
metadata,
autoload_with=engine
)8.2. GET
GET
Ahora ponemos los endpoints de la API
@app.route("/")
def home():
return "API Flask conectada a MySQL (Docker). Prueba /api/productos"
@app.route("/api/productos")
def listar_productos():
stmt = select(
productos_t.c.id,
productos_t.c.nombre,
productos_t.c.descripcion,
productos_t.c.precio,
productos_t.c.stock,
productos_t.c.disponible,
productos_t.c.fecha_creacion,
productos_t.c.categoria_id,
)
with engine.connect() as conn:
rows = conn.execute(stmt).mappings().all()
return jsonify([dict(row) for row in rows])
@app.route("/api/categorias")
def listar_categorias():
stmt = select(categorias_t)
with engine.connect() as conn:
rows = conn.execute(stmt).mappings().all()
return jsonify([dict(row) for row in rows])8.3. POST
Importamos las librería:
from flask import request
from datetime import datetime
Código para productos
@app.route("/api/productos", methods=["POST"])
def crear_producto():
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "No se ha enviado JSON"}), 400
# Campos obligatorios según la tabla
campos = ["nombre", "descripcion", "precio", "stock", "disponible", "categoria_id"]
for campo in campos:
if campo not in data:
return jsonify({"error": f"Falta el campo {campo}"}), 400
with engine.begin() as conn:
result = conn.execute(
productos_t.insert().values(
nombre=data["nombre"],
descripcion=data["descripcion"],
precio=data["precio"],
stock=data["stock"],
disponible=1 if data["disponible"] else 0,
fecha_creacion=datetime.now(),
categoria_id=data["categoria_id"]
)
)
nuevo_id = result.lastrowid
producto = conn.execute(
select(productos_t).where(productos_t.c.id == nuevo_id)
).mappings().first()
return jsonify(dict(producto)), 201
Lo probamos con postman o con cual:
curl -X POST http://127.0.0.1:5000/api/productos \
-H "Content-Type: application/json" \
-d '{
"nombre": "Televison guapa guapa",
"descripcion": "Creado con POST",
"precio": 25.50,
"stock": 8,
"disponible": true,
"categoria_id": 1
}'8.4. PUT
Importamos las librería:
from flask import request
from datetime import datetime
Código para productos
@app.route("/api/productos/<int:pid>", methods=["PUT"])
def actualizar_producto(pid):
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "No se ha enviado JSON"}), 400
# Campos permitidos (no tocamos id ni fecha_creacion)
permitidos = ["nombre", "descripcion", "precio", "stock", "disponible", "categoria_id"]
cambios = {k: data[k] for k in permitidos if k in data}
if "disponible" in cambios:
cambios["disponible"] = 1 if cambios["disponible"] else 0
if not cambios:
return jsonify({"error": "No hay campos válidos para actualizar"}), 400
with engine.begin() as conn:
existe = conn.execute(
select(productos_t.c.id).where(productos_t.c.id == pid)
).first()
if not existe:
return jsonify({"error": "Producto no encontrado"}), 404
conn.execute(
productos_t.update()
.where(productos_t.c.id == pid)
.values(**cambios)
)
producto = conn.execute(
select(productos_t).where(productos_t.c.id == pid)
).mappings().first()
return jsonify(dict(producto))
Lo probamos con postman o con curl:
curl -X PUT http://127.0.0.1:5000/api/productos/6 \
-H "Content-Type: application/json" \
-d '{
"precio": 30.99,
"stock": 20,
"disponible": false
}'8.5. Delete
Código para productos
@app.route("/api/productos/<int:pid>", methods=["DELETE"])
def borrar_producto(pid):
with engine.begin() as conn:
existe = conn.execute(
select(productos_t.c.id).where(productos_t.c.id == pid)
).first()
if not existe:
return jsonify({"error": "Producto no encontrado"}), 404
conn.execute(productos_t.delete().where(productos_t.c.id == pid))
return jsonify({"mensaje": "Producto eliminado"})
Lo probamos con postman o con curl:
curl -X DELETE http://127.0.0.1:5000/api/productos/68.6. Todo
from flask import Flask
from flask import jsonify,request
from datetime import datetime
from flask import render_template
from sqlalchemy import create_engine, MetaData, Table, select
from sqlalchemy.engine import URL
app = Flask(__name__)
DATABASE_URL = URL.create(
drivername="mysql+pymysql",
username="root",
password="ChuckNorris2025",
host="localhost",
port=3307,
database="tienda_virtual",
)
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
)
metadata = MetaData()
productos_t = Table(
"productos_producto",
metadata,
autoload_with=engine
)
categorias_t = Table(
"productos_categoria",
metadata,
autoload_with=engine
)
@app.route("/")
def home():
return "API Flask conectada a MySQL (Docker). Prueba /api/productos"
@app.route("/api/productos")
def listar_productos():
stmt = select(
productos_t.c.id,
productos_t.c.nombre,
productos_t.c.descripcion,
productos_t.c.precio,
productos_t.c.stock,
productos_t.c.disponible,
productos_t.c.fecha_creacion,
productos_t.c.categoria_id,
)
with engine.connect() as conn:
rows = conn.execute(stmt).mappings().all()
return jsonify([dict(row) for row in rows])
@app.route("/api/categorias")
def listar_categorias():
stmt = select(categorias_t)
with engine.connect() as conn:
rows = conn.execute(stmt).mappings().all()
return jsonify([dict(row) for row in rows])
@app.route("/api/productos", methods=["POST"])
def crear_producto():
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "No se ha enviado JSON"}), 400
# Campos obligatorios según la tabla
campos = ["nombre", "descripcion", "precio", "stock", "disponible", "categoria_id"]
for campo in campos:
if campo not in data:
return jsonify({"error": f"Falta el campo {campo}"}), 400
with engine.begin() as conn:
result = conn.execute(
productos_t.insert().values(
nombre=data["nombre"],
descripcion=data["descripcion"],
precio=data["precio"],
stock=data["stock"],
disponible=1 if data["disponible"] else 0,
fecha_creacion=datetime.now(),
categoria_id=data["categoria_id"]
)
)
nuevo_id = result.lastrowid
producto = conn.execute(
select(productos_t).where(productos_t.c.id == nuevo_id)
).mappings().first()
return jsonify(dict(producto)), 201
@app.route("/api/productos/<int:pid>", methods=["PUT"])
def actualizar_producto(pid):
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "No se ha enviado JSON"}), 400
# Campos permitidos (no tocamos id ni fecha_creacion)
permitidos = ["nombre", "descripcion", "precio", "stock", "disponible", "categoria_id"]
cambios = {k: data[k] for k in permitidos if k in data}
if "disponible" in cambios:
cambios["disponible"] = 1 if cambios["disponible"] else 0
if not cambios:
return jsonify({"error": "No hay campos válidos para actualizar"}), 400
with engine.begin() as conn:
existe = conn.execute(
select(productos_t.c.id).where(productos_t.c.id == pid)
).first()
if not existe:
return jsonify({"error": "Producto no encontrado"}), 404
conn.execute(
productos_t.update()
.where(productos_t.c.id == pid)
.values(**cambios)
)
producto = conn.execute(
select(productos_t).where(productos_t.c.id == pid)
).mappings().first()
return jsonify(dict(producto))
@app.route("/api/productos/<int:pid>", methods=["DELETE"])
def borrar_producto(pid):
with engine.begin() as conn:
existe = conn.execute(
select(productos_t.c.id).where(productos_t.c.id == pid)
).first()
if not existe:
return jsonify({"error": "Producto no encontrado"}), 404
conn.execute(productos_t.delete().where(productos_t.c.id == pid))
return jsonify({"mensaje": "Producto eliminado"})
if __name__ == "__main__":
app.run(debug=True)9. Cerramos la app
En el prompt
deactivate