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.4

3. 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

Ejercicios para ver su simplicidad 

1) Crear una ruta /hora que devuelva la hora actual.
 
2) Crear una ruta /cuadrado/<numero> que muestre el cuadrado de un número.

3) Añadir un producto nuevo a la lista y mostrarlo en /api/productos.

4) Crear una plantilla saludo.html que muestre un mensaje recibido por URL.

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:

http://127.0.0.1:5000/hora

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/7

7.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/productos

7.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/Pepe

8. 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/6

8.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