Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 66: Tu propia API REST con Flask

100 Días de Código en Python — Día 66: Tu propia API REST con Flask

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 66 (nivel avanzado).

Este tutorial adapta pedagógicamente el Día 66. Las secciones 📝 Contenido original del curso y 📝 Ejercicio original del curso provienen de las lecciones 464, 465 y 467 a 475 (la lección 466, con los archivos de inicio, no está entre los archivos suministrados; el proyecto parte de una base de datos cafes.db de cafeterías). Las soluciones oficiales están en los gists enlazados y el proyecto final (day-66-cafe-api-end.zip) en los recursos. Los retos 🟢🟡🟠🔴, las secciones sobre códigos de estado y seguridad de la clave, el proyecto extra y la etapa 8 del proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14, Flask 3.1.3 y Flask-SQLAlchemy 3.1.1, se construyó una API de cafeterías equivalente a la del curso (con datos colombianos de prueba) y se probaron todas sus rutas con el cliente de pruebas de Flask. Se comprobó: que jsonify ordena las claves alfabéticamente y escapa las tildes (Usaquén), que la búsqueda por ubicación distingue mayúsculas, que bool("0") es True (un error típico al leer casillas de un formulario), que un nombre repetido provoca un error 500, los códigos 403, 404 y 405, y que httpstatuses.com, enlazado por el curso, ya no muestra la lista de códigos (redirige a otro sitio).


Tabla de contenidos

  1. Antes de empezar: de usar APIs a crear una
  2. ¿Qué es REST?
  3. GET: una cafetería al azar y serialización a JSON
  4. GET: todas las cafeterías y búsqueda por ubicación
  5. Probar la API: Postman y alternativas
  6. POST: agregar una cafetería
  7. PUT contra PATCH, y PATCH del precio
  8. DELETE con clave de API
  9. 📖 Los códigos de estado HTTP
  10. Documentar la API
  11. 🚀 Proyecto del día: la API de Cafés y Wifi
  12. 🌟 Proyecto extra: la API del menú del restaurante escolar
  13. 🏗️ Proyecto integrador del bloque avanzado — Etapa 8
  14. 🧩 Comprueba lo aprendido
  15. 🤖 Programar con apoyo de IA
  16. 🛠️ Correcciones técnicas al material original
  17. 📋 Matriz de trazabilidad
  18. 📦 Evidencias de aprendizaje del Día 66

0. Antes de empezar: de usar APIs a crear una

(Fuente: 464-day-66-goals-build-your-own-rest-api-service.md)

📝 Contenido original del curso

Desde el Día 33 has usado APIs públicas (la ubicación de la ISS, preguntas de trivia, Twilio). Muchas empresas reúnen datos valiosos (precios de bitcoin, reseñas de restaurantes) y cobran por el acceso a su API: de 9 a 99 dólares al mes, o por cada llamada. ¿Y si tú tienes datos que otros quieren? Por ejemplo, las cafeterías de una ciudad aptas para trabajar con el portátil. Podrías crear una API y cobrar por el acceso. Hoy construyes una API REST completa con Flask.

Prerequisitos: APIs desde el lado del cliente con requests (Días 33 a 37), GET y POST en Flask (Día 60), CRUD con SQLAlchemy (Días 63 y 64), diccionarios y JSON (Días 9 y 30).

📖 Página web contra API (adaptación)

Página web (Días 54 a 64) API (hoy)
¿Quién la usa? Una persona con un navegador Un programa (otra aplicación, un script)
¿Qué devuelve? HTML para mostrar Datos (JSON) para procesar
En Flask render_template(...) jsonify(...)

1. ¿Qué es REST?

(Fuente: 465 - What is REST-.md)

🎯 Objetivo de aprendizaje

Explicar qué es un estilo arquitectónico de APIs y cuáles son las dos reglas principales de REST.

📝 Contenido original del curso

REST no es "descansar en el sofá viendo Netflix": significa REpresentational State Transfer (transferencia de estado representacional). Para entenderlo, volvemos a la arquitectura cliente-servidor con la analogía del restaurante: el cliente pide una pizza; el mesero revisa si el restaurante puede cumplir. Si pide algo que no está en el menú (una salchicha), recibe un 404: el recurso no existe.

  • La solicitud se hace en un "idioma": HTTP. Hay otros, como FTP (transferencia de archivos); si hablas el idioma equivocado, no te entienden.
  • HTTPS es HTTP seguro: la solicitud va cifrada, como si tú y el mesero hablaran klingon; aunque alguien escuche, no entiende. Indispensable al enviar datos de una tarjeta.
  • Para responder, el servidor puede calcular algo o consultar la base de datos, o ambas.
  • Una API es como el menú: la lista de cosas que el servidor sabe responder.

REST es un estilo arquitectónico, como el barroco, el gótico o el neoclásico en los edificios. Hay otros estilos para APIs: SOAP (el dominante antes de REST), GraphQL, Falcor… pero REST es "el estándar de oro". Lo propuso Roy Fielding en su tesis doctoral en la Universidad de California: si todas las APIs siguieran las mismas reglas, sería fácil usarlas todas (como si en todos los edificios los baños estuvieran en el mismo lugar).

Las dos reglas más importantes:

  1. Usar los verbos HTTP, que se corresponden con CRUD:

    Verbo HTTP CRUD Ejemplo
    GET Read Ver artículos
    POST Create Crear un artículo
    PUT Update (reemplazar todo) Reemplazar un artículo
    PATCH Update (cambiar una parte) — agregado a HTTP en 2010 Cambiar solo el título
    DELETE Delete Borrar
  2. Usar un patrón de rutas específico (como las rutas de un safari hacia los elefantes o las jirafas). Con el ejemplo de una API tipo Wikipedia:

    Ruta GET POST PUT / PATCH DELETE
    /articles Todos los artículos Crear un artículo nuevo — Borrar todos
    /articles/jack-bauer Ese artículo — Actualizar ese artículo Borrar ese artículo

📖 Qué observar (adaptación)

  • En REST, la ruta nombra un recurso (un sustantivo: /cafes, /cafes/22) y el verbo dice qué hacer con él. Por eso una ruta como /update-price o /report-closed (verbos en la URL), que usa el curso, es menos REST que PATCH /cafes/22. Lo verás en los retos.
  • "DELETE /articles borra todos": en una API real, eso casi nunca se expone, y si existe, se protege mucho.

2. GET: una cafetería al azar y serialización a JSON

(Fuente: 467-http-get-a-random-cafe.md)

🎯 Objetivo de aprendizaje

Devolver un registro de la base de datos como JSON (serializar) desde una ruta GET.

📝 Ejercicio original del curso

Un caso de uso probable: un desarrollador quiere mostrar a su usuario una cafetería al azar.

  1. Crea la ruta /random que acepte GET.
  2. Al recibir la solicitud, obtén una cafetería al azar de la base de datos (sin devolver nada todavía).
  3. Hasta ahora devolvías plantillas con render_template(), pero una API devuelve JSON, como la API de la ISS. Convertir el objeto de SQLAlchemy a JSON se llama serialización. Flask trae jsonify(), pero tú defines la estructura. Con su documentación, haz que /random funcione.

Dos formas de serializar:

  • Escribir la estructura a mano: máxima flexibilidad; puedes omitir campos (como id) o agrupar los booleanos en una subsección amenities.
  • Convertir el objeto a diccionario y pasárselo a jsonify(), para no escribir todos los campos en cada ruta.

📖 Explicación conceptual

Python
import random
from flask import jsonify


class Cafe(db.Model):
    ...
    def to_dict(self):
        # Un diccionario {nombre_de_columna: valor} con todas las columnas de la tabla
        return {column.name: getattr(self, column.name) for column in self.__table__.columns}


@app.route("/random")
def get_random_cafe():
    cafes = db.session.execute(db.select(Cafe)).scalars().all()
    random_cafe = random.choice(cafes)
    return jsonify(cafe=random_cafe.to_dict())
  • self.__table__.columns es la lista de columnas del modelo; getattr(objeto, "name") equivale a objeto.name.
  • jsonify(cafe=...) crea {"cafe": {...}} y pone la cabecera Content-Type: application/json (verificado).

Respuesta verificada:

JSON
{"cafe":{"coffee_price":null,"has_wifi":false,"id":2,"location":"Usaquén","name":"Otro"}}

Tres detalles:

  • Las claves salen en orden alfabético (Flask ordena por defecto; se puede cambiar con app.json.sort_keys = False).
  • None de Python se vuelve null, y False, false.
  • Las tildes salen escapadas (é): es JSON válido y cualquier cliente lo convierte a "é". Para verlas tal cual: app.json.ensure_ascii = False.

💡 Desde Flask 1.1, una ruta también puede devolver un diccionario directamente (return {"cafe": ...}) y Flask lo convierte a JSON. jsonify sigue siendo útil para listas y para ser explícito.

🛠️ Nota técnica: elegir al azar cargando todas las cafeterías con .all() está bien para decenas de registros. Con miles, conviene pedirle el azar a la base de datos: db.select(Cafe).order_by(db.func.random()).limit(1).


3. GET: todas las cafeterías y búsqueda por ubicación

(Fuentes: 468-http-get-all-the-cafes.md y 469-http-get-find-a-cafe.md)

📝 Ejercicio original del curso

Todas (/all). Un sitio que liste todas las cafeterías (como laptopfriendly.co/london) necesita pedirlas todas. Crea /all que devuelva todas las cafeterías como JSON. Pista: .execute() con .select() y .scalars() para obtener una lista.

Búsqueda (/search). Crea /search para buscar cafeterías en una zona (el campo location de cafes.db). La ubicación llega como parámetro en la URL después de ?: /search?loc=Peckham. Si la zona no existe, devuelve un error en JSON. Pista: filtra con .where().

📖 Explicación conceptual

Python
@app.route("/all")
def get_all_cafes():
    cafes = db.session.execute(db.select(Cafe).order_by(Cafe.name)).scalars().all()
    return jsonify(cafes=[cafe.to_dict() for cafe in cafes])


@app.route("/search")
def search_cafes():
    location = request.args.get("loc")
    cafes = db.session.execute(db.select(Cafe).where(Cafe.location == location)).scalars().all()
    if cafes:
        return jsonify(cafes=[cafe.to_dict() for cafe in cafes])
    return jsonify(error={"Not Found": "Sorry, we don't have a cafe at that location."}), 404
  • request.args lee los parámetros de consulta (Día 60).
  • Devolver una tupla (json, 404) envía el código de estado correcto.

Comportamiento verificado:

Solicitud Código
/search?loc=Chapinero 200 con la lista
/search?loc=chapinero (minúsculas) 404: la comparación distingue mayúsculas
/search sin parámetro 404 (loc es None)

Para buscar sin distinguir mayúsculas: where(db.func.lower(Cafe.location) == location.lower()). Y si falta el parámetro, lo más claro es responder 400 ("falta el parámetro loc").

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1 — Aplicación: haz la búsqueda insensible a mayúsculas y responde 400 si falta loc.
  • 🟡 Reto 2 — Ampliación: agrega filtros opcionales: /search?loc=Chapinero&wifi=1&sockets=1.
  • 🟠 Reto 3 — Desafío: pagina /all: /all?page=2&per_page=5, con los metadatos page, total y pages en la respuesta.
  • 🔴 Reto 4 — Pensamiento computacional: rediseña las rutas del curso al estilo REST "puro" (GET /cafes, GET /cafes/<id>, GET /cafes?loc=..., POST /cafes, PATCH /cafes/<id>, DELETE /cafes/<id>) y compara en una tabla con las del curso.

4. Probar la API: Postman y alternativas

(Fuente: 470-postman-the-all-in-one-api-testing-tool.md)

📝 Contenido original del curso

Probar una API escribiendo parámetros en la barra del navegador es cansado y propenso a errores. Postman (gratuito) permite escribir los parámetros como pares clave-valor (arma la URL solo) y generar documentación automáticamente. Crea una colección llamada "Cafe & Wifi" y guarda en ella las 3 rutas; es importante para generar la documentación después. Prueba pedir una cafetería al azar desde Postman.

📖 Alternativas para el aula (adaptación)

Postman pide crear una cuenta. Otras opciones:

Herramienta Ventaja
Python + requests (Día 33) Ya lo sabes; sirve para pruebas automáticas
El cliente de pruebas de Flask (app.test_client()) No necesita ni arrancar el servidor; ideal para assert
curl en la terminal Viene en Windows 10+, Mac y Linux
Extensiones de VS Code (por ejemplo, Thunder Client) o Bruno Parecidas a Postman, sin salir del editor
Python
import requests

BASE = "http://127.0.0.1:5000"
print(requests.get(f"{BASE}/random").json())
print(requests.get(f"{BASE}/search", params={"loc": "Chapinero"}).status_code)

5. POST: agregar una cafetería

(Fuente: 471-http-post-a-new-cafe.md)

📝 Ejercicio original del curso

¿Y si los usuarios pudieran sugerir cafeterías (como laptopfriendly.co/suggest)? El POST vendría de un formulario, pero se puede simular con Postman: los pares clave-valor de la pestaña Body equivalen a los <input name="..."> de un formulario. "Es tan bueno haciendo POST que algunos lo llaman POSTman". Crea la ruta sin WTForms.

📖 Explicación conceptual

Python
@app.route("/add", methods=["POST"])
def post_new_cafe():
    new_cafe = Cafe(
        name=request.form.get("name"),
        map_url=request.form.get("map_url"),
        location=request.form.get("loc"),
        has_wifi=a_booleano(request.form.get("wifi")),
        ...
    )
    db.session.add(new_cafe)
    db.session.commit()
    return jsonify(response={"success": "Successfully added the new cafe."}), 201

🛠️ Nota técnica (verificado): los datos de un formulario llegan siempre como texto. bool("0") es True (cualquier texto no vacío es verdadero), así que una cafetería enviada con wifi=0 queda registrada con wifi. Convierte explícitamente:

Python
def a_booleano(valor):
    return str(valor).strip().lower() in ("1", "true", "sí", "si", "on")

🛠️ Nota técnica (verificado): si se envía un nombre que ya existe (columna unique), el commit() lanza IntegrityError y la API responde 500. Atrapa la excepción, haz db.session.rollback() y responde 409 Conflict o 400 con un mensaje claro. Lo mismo si falta un campo obligatorio.

¿Formulario o JSON? Muchas APIs reciben los datos como JSON en el cuerpo (Content-Type: application/json). En Flask se leen con request.get_json(). Con requests: requests.post(url, json={...}) (JSON) o requests.post(url, data={...}) (formulario).


6. PUT contra PATCH, y PATCH del precio

(Fuentes: 472 - HTTP PUT vs. PATCH.md y 473-http-patch-a-cafe-s-coffee-price.md)

📝 Contenido original del curso

La analogía de la bicicleta. La profesora compró una bicicleta que llegó con la rueda delantera dañada. Amazon ofreció dos soluciones: enviar una bicicleta completa nueva (PUT: reemplazar todo el registro) o enviar solo la rueda (PATCH: enviar solo lo que cambia, "y salvar al planeta de emisiones").

El ejercicio. Los precios de las cafeterías cambian. Si el usuario conoce el id (lo obtiene de /all), puede actualizar solo coffee_price: un PATCH es lo más eficiente.

  1. Crea una ruta PATCH /update-price/<cafe_id>; por ejemplo, /update-price/22.
    • Pista 1: usa .get() con try/except para obtener la cafetería por id; si hay AttributeError, devuelve un error en JSON.
    • Pista 2: el nuevo precio llega como parámetro de la solicitud.
  2. Si el id no existe, informa al usuario.

Nota del curso: aunque el recurso no se encuentre, la respuesta devuelve 200 ("todo bien"); debería ser 404. El curso muestra cómo devolver un código HTTP con la respuesta.

📖 Explicación conceptual (verificada)

Python
@app.route("/update-price/<int:cafe_id>", methods=["PATCH"])
def patch_new_price(cafe_id):
    cafe = db.session.get(Cafe, cafe_id)
    if cafe is None:
        return jsonify(error={"Not Found": "Sorry a cafe with that id was not found in the database."}), 404
    cafe.coffee_price = request.args.get("new_price")
    db.session.commit()
    return jsonify(response={"success": "Successfully updated the price."}), 200
  • db.session.get(Cafe, id) devuelve None si no existe. Comprobar if cafe is None es más claro que esperar un AttributeError.
  • Visitar /update-price/1 con GET (en el navegador) da 405 Method Not Allowed: la ruta solo acepta PATCH (verificado).

🛠️ Nota técnica: el nuevo precio viaja en la URL (?new_price=...). Funciona, pero lo habitual en REST es enviarlo en el cuerpo de la solicitud (formulario o JSON), como en el POST.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: valida que new_price exista y no esté vacío; si no, responde 400.
  • 🟡 Reto 2: haz una ruta PUT /cafes/<id> que reemplace todos los campos y responda 400 si falta alguno. Compárala con el PATCH.
  • 🟠 Reto 3: haz un PATCH general que actualice solo los campos que lleguen (name, location, coffee_price…) e ignore los desconocidos.

7. DELETE con clave de API

(Fuente: 474-http-delete-a-cafe-that-s-closed.md)

📝 Ejercicio original del curso

Cuando una cafetería cierra, hay que borrarla. Pero no cualquiera debería poder borrar: "podríamos terminar con alguien que borra todo". Medida de seguridad: exigir una api-key. Si es "TopSecretAPIKey", se permite; si no, se responde 403 (no autorizado). La lista de códigos está en httpstatuses.com.

  1. Crea la ruta DELETE /report-closed/<cafe_id>, con tres casos: clave correcta (borrado), clave incorrecta (403) e id inexistente (404).

📖 Explicación conceptual (verificada)

Python
import os

API_KEY = os.environ.get("CAFE_API_KEY", "TopSecretAPIKey")   # la del curso solo para practicar


@app.route("/report-closed/<int:cafe_id>", methods=["DELETE"])
def delete_cafe(cafe_id):
    if request.args.get("api-key") != API_KEY:
        return jsonify(error="Sorry, that's not allowed. Make sure you have the correct api_key."), 403
    cafe = db.get_or_404(Cafe, cafe_id)
    db.session.delete(cafe)
    db.session.commit()
    return jsonify(response={"success": "Successfully deleted the cafe from the database."}), 200

Resultados verificados: sin clave → 403; con clave → 200; borrar el mismo id otra vez → 404.

🛠️ Nota técnica (seguridad): la clave del curso viaja en la URL (?api-key=...), y las URL quedan guardadas en el historial, en los registros del servidor y en las herramientas intermedias. En APIs reales la clave se envía en una cabecera (por ejemplo, X-API-Key o Authorization), siempre sobre HTTPS, y no se escribe en el código. Además, una sola clave compartida por todos no permite saber quién borró; lo resolverás con usuarios el Día 68.

🛠️ Nota técnica: estrictamente, 401 Unauthorized significa "no te identificaste" (falta la clave) y 403 Forbidden, "sé quién eres, pero no tienes permiso". Muchas APIs usan 401 para una clave ausente o inválida. El curso usa 403; ambas son defendibles si se documentan.


8. 📖 Los códigos de estado HTTP

(Sección creada para esta adaptación, a partir de las lecciones 469, 473 y 474.)

🛠️ Nota técnica (verificado): httpstatuses.com, enlazado por el curso, ahora redirige a un glosario de otra empresa. Una referencia confiable es la documentación de MDN ("HTTP response status codes").

Código Nombre Cuándo usarlo en esta API
200 OK Lectura, actualización o borrado exitosos
201 Created Se creó una cafetería (POST)
400 Bad Request Faltan datos o tienen mal formato
401 Unauthorized Falta la clave o no es válida
403 Forbidden La clave es válida, pero no tiene permiso
404 Not Found El recurso (id o zona) no existe
405 Method Not Allowed Verbo equivocado para esa ruta (Flask lo hace solo)
409 Conflict Ya existe (nombre repetido)
500 Internal Server Error Un error tuyo sin manejar: nunca debería verlo un cliente

Las familias: 2xx éxito, 4xx error del cliente, 5xx error del servidor.


9. Documentar la API

(Fuente: 475-build-documentation-for-your-api.md)

📝 Contenido original del curso

Si quieres que otros usen tu API, tienes que documentarla: nadie ve tu código, así que hay que explicar las rutas, los parámetros obligatorios, etc. Si hiciste todas las solicitudes en Postman con nombre y descripción, Postman genera la documentación:

  1. Comprueba que todas las solicitudes funcionan.
  2. Guárdalas en la misma colección ("Cafe & Wifi").
  3. En los tres puntos de la colección, Publish Docs.
  4. Publica (ejemplo del curso: documenter.getpostman.com/view/2568017/TVRhd9qR).
  5. Agrega en index.html un enlace a la documentación.

📖 Qué debe tener una buena documentación (adaptación)

Si no usas Postman, un API.md en el proyecto sirve igual. Por cada ruta:

Markdown
### GET /search
Busca cafeterías por zona.

| Parámetro | Dónde | Obligatorio | Ejemplo |
|---|---|---|---|
| loc | URL (query) | Sí | Chapinero |

Respuestas: 200 con `{"cafes": [...]}` · 400 si falta `loc` · 404 si no hay cafeterías en esa zona.

Ejemplo: `curl "http://127.0.0.1:5000/search?loc=Chapinero"`

10. 🚀 Proyecto del día: la API de Cafés y Wifi

(Integra las lecciones 467 a 475.)

Problema

Tienes una base de datos valiosa (cafeterías aptas para trabajar) y quieres que otras aplicaciones la usen, sin darles acceso directo a tu base de datos.

Objetivo

Una API REST en Flask con las 7 rutas del curso, probada y documentada.

Requisitos funcionales

Ruta Verbo Función
/random GET Una cafetería al azar
/all GET Todas
/search?loc=... GET Por zona; 404 si no hay
/add POST Crear; 201
/update-price/<id>?new_price=... PATCH Cambiar el precio; 404 si no existe
/report-closed/<id>?api-key=... DELETE Borrar con clave; 403 y 404
Documentación — Publicada (Postman o API.md) y enlazada desde index.html

Restricciones técnicas

  • Serialización con un método to_dict() en el modelo.
  • Códigos de estado correctos (sección 8); ningún 500 en los casos de prueba.
  • Booleanos del formulario convertidos explícitamente.
  • La clave de la API en una variable de entorno.

Planificación

  1. Comprender la base de datos (ábrela en DB Browser: ¿qué columnas tiene?). 2. Entradas: parámetros y cuerpos. 3. Procesos: CRUD. 4. Salidas: JSON y códigos. 5. Dividir: una ruta a la vez, probándola antes de seguir. 6. Diseñar la tabla de pruebas. 7. Implementar. 8. Probar. 9. Corregir. 10. Documentar.

Construcción paso a paso

  • Versión 1: /random con JSON escrito a mano.
  • Versión 2: to_dict(), /all y /search.
  • Versión 3: /add con prueba en Postman o con requests.
  • Versión 4: PATCH y DELETE con sus errores.
  • Versión final: pruebas automáticas y documentación.

Pruebas (automáticas, con el cliente de pruebas de Flask)

Python
from main import app

cliente = app.test_client()
assert cliente.get("/random").status_code == 200
assert cliente.get("/search?loc=ZonaQueNoExiste").status_code == 404
assert cliente.patch("/update-price/99999?new_price=1").status_code == 404
assert cliente.delete("/report-closed/1").status_code == 403       # sin clave
assert cliente.get("/update-price/1").status_code == 405           # verbo equivocado
print("Todas las pruebas pasaron")

(Usa una copia de la base de datos para las pruebas que borran o modifican.)

🧪 Prueba y depuración

Python
@app.route("/report-closed/<cafe_id>")
def delete_cafe(cafe_id):
    if request.args.get("api-key") == "TopSecretAPIKey":
        cafe = db.session.get(Cafe, cafe_id)
        db.session.delete(cafe)
        return jsonify(success="Deleted")
    return jsonify(error="Not allowed")

Encuentra cinco problemas: la ruta no declara methods=["DELETE"]; falta int:; si el id no existe, delete(None) falla (500); falta db.session.commit(); la respuesta de error sale con 200 en lugar de 403.

⭐ Extensión del proyecto

  • Un script cliente cliente.py que use la API para mostrar en consola las 3 cafeterías más baratas de una zona.
  • Limitar la cantidad de solicitudes por minuto (investiga rate limiting y la extensión Flask-Limiter).

11. 🌟 Proyecto extra: la API del menú del restaurante escolar

(Proyecto creado para esta adaptación; no existe en el curso original.)

Situación

La aplicación del colegio (y un bot del grupo del curso) quieren mostrar el menú del día. En el Día 57 hiciste páginas con el menú; ahora el restaurante necesita una API para que otros programas lo consulten.

Requisitos

  1. Modelo Almuerzo (fecha, plato, bebida, postre, alérgenos como texto separado por comas, calorías aproximadas).
  2. Rutas: GET /menu/hoy, GET /menu?fecha=AAAA-MM-DD, GET /menu/semana, GET /menu/sin/<alergeno>, POST /menu (con clave, para el restaurante), PATCH /menu/<id> (con clave) y DELETE /menu/<id> (con clave).
  3. La clave se envía en la cabecera X-API-Key y se lee de una variable de entorno.
  4. Fechas inválidas → 400 con un mensaje que explique el formato.
  5. API.md con todas las rutas, parámetros, respuestas y ejemplos con curl.
  6. Un cliente en Python que imprima el menú de hoy de forma bonita.

Decisiones que debes tomar (y justificar)

  • ¿Qué devuelve /menu/hoy un sábado: 404 o 200 con un mensaje?
  • ¿Guardas los alérgenos como texto o en otra tabla? (Pista: Día 69.)

Casos de prueba

  • POST /menu sin clave → 401 o 403 (según lo que documentes).
  • GET /menu?fecha=27-09-2026 → 400.
  • GET /menu/sin/maní no devuelve ningún almuerzo con maní.

12. 🏗️ Proyecto integrador del bloque avanzado — Etapa 8

(Proyecto creado para esta adaptación. Descripción completa en el tutorial del Día 70.)

La Plataforma del Club — Etapa 8 (hoy):

  1. Agrega una API bajo el prefijo /api: GET /api/actividades, GET /api/actividades/<id> y GET /api/actividades?desde=AAAA-MM-DD.
  2. POST, PATCH y DELETE protegidos con una clave en la cabecera X-API-Key (en .env).
  3. Códigos de estado correctos y pruebas automáticas con el cliente de pruebas de Flask.
  4. API.md con la documentación.

13. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué significa REST y quién lo propuso?
  2. Relaciona los cinco verbos HTTP con las operaciones CRUD.
  3. Explica PUT y PATCH con la analogía de la bicicleta.
  4. ¿Qué es serializar? ¿Qué hace jsonify?
  5. ¿Por qué no es buena idea enviar una clave de API en la URL?

Análisis de código

Python
return jsonify(error={"Not Found": "No existe"})

¿Qué código de estado recibe el cliente? ¿Por qué es un problema?

Predicción

Python
print(bool("0"), bool(""), bool("False"), bool(0))

Depuración

Un compañero dice: "Mi ruta PATCH funciona en Postman, pero en el navegador me sale Method Not Allowed". ¿Qué le explicas?

Modificación

Cambia /random para que no incluya el id y agrupe los booleanos (has_wifi, has_sockets…) en un objeto amenities, como sugiere el curso.

Creación

Escribe una ruta GET /stats que devuelva cuántas cafeterías hay por zona, en formato {"Chapinero": 3, "Usaquén": 1}.


14. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.

Actividad 1 — Códigos de estado. Pide a una IA una ruta Flask para borrar un registro. ¿Devuelve 404 si no existe? ¿Usa DELETE o GET? Pruébala con el cliente de pruebas y compara con la tabla de la sección 8 y con MDN.

Actividad 2 — El booleano traicionero. Pregunta a una IA cómo leer una casilla de verificación de un formulario en Flask. ¿Usa bool(request.form.get(...))? Si sí, muéstrale el caso "0".

Actividad 3 — Documentar. Da a una IA tu código y pídele que genere API.md. Revisa cada ruta: ¿los parámetros, códigos y ejemplos coinciden con lo que tu código hace de verdad? Corrige lo que inventó.


15. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
466 Lección de archivos de inicio No está entre los archivos suministrados Se indica en la cabecera
473 Pista: usar .get() y atrapar AttributeError Con SQLAlchemy 2, db.session.get() devuelve None; es más claro comprobar None y responder 404 Sección 6
473 Precio nuevo como parámetro en la URL Lo habitual es enviarlo en el cuerpo Nota técnica de la sección 6
474 Clave "TopSecretAPIKey" en la URL y en el código Debe ir en una cabecera, sobre HTTPS, y en una variable de entorno Nota técnica de la sección 7
474 403 para clave incorrecta 401 también es válido; se explican ambos Nota técnica de la sección 7
474 Enlace a httpstatuses.com Ya no muestra los códigos (verificado); se recomienda MDN Sección 8
471 Crear la cafetería con los datos del formulario bool("0") es True y un nombre repetido da error 500 (verificado) Notas técnicas de la sección 5
467 jsonify con la estructura del objeto Las claves salen ordenadas alfabéticamente y las tildes escapadas (verificado) Sección 2
465 "DELETE /articles borra todos los artículos" Es el patrón teórico; en la práctica casi nunca se expone Sección 1
470, 475 Postman Requiere cuenta; se ofrecen alternativas Secciones 4 y 9

16. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
APIs usadas desde el Día 33, APIs de pago, crear una API de cafeterías 464 Sección 0 — Página web contra API Sección 10
REST, cliente-servidor, HTTP, FTP, HTTPS, API como menú, estilos (SOAP, GraphQL, Falcor), Roy Fielding, verbos HTTP y CRUD, patrón de rutas 465 Sección 1 — Sustantivos en las rutas Secciones 10, 11 y 12
(Archivos de inicio; no suministrado) 466 — — — —
/random, serialización, jsonify, estructura a mano, amenities, conversión a diccionario 467 Sección 2 GET aleatorio Orden de claves y tildes Sección 10
/all, .execute(), .select(), .scalars() 468 Sección 3 GET todas Paginación Sección 10
/search?loc=, parámetros, .where(), error si no existe 469 Sección 3 GET buscar Mayúsculas, filtros, 400; retos 1–4 Secciones 10 y 11
Postman, colección "Cafe & Wifi" 470 Sección 4 Probar con Postman Alternativas y cliente en Python Sección 10
POST sin WTForms, Body de Postman como <input> 471 Sección 5 POST nueva cafetería Booleanos, duplicados, JSON Sección 10
PUT contra PATCH (la bicicleta) 472 Sección 6 — PUT completo Sección 10
PATCH /update-price/<id>, try/except, precio como parámetro, 404 en lugar de 200 473 Sección 6 PATCH del precio Retos 1–3 de la sección 6 Sección 10
DELETE /report-closed/<id>, api-key, 403, 404, httpstatuses.com 474 Secciones 7 y 8 DELETE con clave Clave en cabecera; 401 contra 403 Secciones 10, 11 y 12
Documentación con Postman, Publish Docs, enlace en index.html 475 Sección 9 Documentar API.md Secciones 10, 11 y 12
(No existe en el curso original) — — — API del menú del restaurante Sección 11
(No existe en el curso original) — — — Plataforma del Club, etapa 8 Sección 12

17. 📦 Evidencias de aprendizaje del Día 66

  • La API de Cafés y Wifi con las 7 rutas y el archivo de pruebas automáticas pasando.
  • La documentación publicada (Postman) o API.md.
  • Un cliente en Python que consuma tu propia API.
  • La API del menú del restaurante escolar (proyecto extra).
  • La etapa 8 de la Plataforma del Club.
  • Cuaderno de errores: 405 por verbo equivocado, respuestas de error con 200, bool("0"), nombre repetido con error 500, delete(None), commit() olvidado, búsqueda que distingue mayúsculas, clave de API en la URL, render_template en lugar de jsonify.