CS50x en Español - Clase 9 - Flask
CS50
La semana de síntesis con Flask 0:43
Esta novena semana funciona como una síntesis de todo lo aprendido hasta ahora, de forma parecida a como la semana 6 tradujo conceptos de C a Python. El objetivo es aplicar esas herramientas a la programación web, cerrando el círculo entre el HTML y JavaScript del lado del cliente vistos la semana pasada y un componente del lado del servidor escrito en Python y SQL, con el que se construirán aplicaciones web completas y, si se desea, también móviles para el proyecto final. Se recuerda que hasta ahora las URL apuntaban literalmente a archivos o carpetas en el servidor, pero a partir de hoy se hablará de rutas, porque en la programación web se controla lo que aparece después del nombre de dominio, incluyendo parámetros pasados con signos de interrogación y ampersands, como en el ejemplo de una búsqueda en Google que en realidad envía una solicitud a una ruta llamada slash search con el parámetro q igual a cats.
Por qué usar Flask en vez de construir todo 4:00
Escribir un servidor web desde cero en C sería una pesadilla, y hacerlo en Python también implicaría mucho trabajo para analizar las solicitudes. Por eso el mundo usa servidores de aplicaciones ya hechos, y en este curso se usará Flask, descrito como un framework, o más específicamente un microframework: una biblioteca de código escrita por otros que además viene con un conjunto de convenciones para usarla correctamente. En lugar de ejecutar http-server como la semana pasada, ahora se ejecuta el comando flask run, que busca el código en el directorio actual y, si sigue las convenciones, inicia la aplicación en un puerto TCP, por defecto el 5000. Para que esto funcione basta con tener un archivo llamado app.py y, opcionalmente, un archivo requirements.txt donde se lista, una por línea, cada biblioteca que la aplicación necesita, evitando así tener que escribir manualmente el comando pip install para cada una.
La estructura mínima de una app en Flask 7:31
El código mínimo de app.py importa la función Flask del paquete flask, crea una variable llamada app pasando el nombre del archivo actual, y define una función llamada index que devuelve el texto hello world. La sintaxis nueva es el decorador de Python, escrito como arroba app.route seguido de la ruta entre comillas, que le indica a Flask que asocie esa función con esa ruta, en este caso la barra diagonal simple. Al ejecutar flask run y abrir la página, el código fuente muestra que el navegador solo recibió esa línea de texto, no HTML real, aunque el navegador lo renderiza como si fuera una página mínima. Se muestra luego que es posible devolver directamente una cadena de HTML completa, con doctype, head, title y body, y que el navegador entonces sí recibe ese HTML tal cual.
Separar el HTML en plantillas 14:30
Poner HTML codificado dentro de una cadena en Python resulta poco práctico, así que se introduce la función render_template, que también viene con Flask. Su función es renderizar una plantilla de HTML guardada en un archivo separado, por convención dentro de una carpeta llamada templates, que debe coexistir con app.py y requirements.txt. Así, el archivo index.html vuelve a contener el mismo HTML de antes, pero ahora vive separado de la lógica en Python, replicando la misma idea de factorización que se aplicó antes a JavaScript y CSS.
Variables de la URL insertadas en la plantilla 17:05
Para tomar entrada real del usuario a través de la URL, como name=David después de un signo de interrogación, Flask ofrece una variable global llamada request.args, un diccionario con los parámetros pasados en la solicitud HTTP. El valor se extrae con request.args seguido de la clave entre corchetes y se pasa a render_template como un argumento con nombre, por ejemplo placeholder. Dentro de index.html, ese valor se inserta usando un par de llaves dobles alrededor de la palabra placeholder, sintaxis que pertenece a Jinja, la biblioteca de plantillas incluida en Flask, cuya única tarea aquí es interpolar variables. A diferencia de los ejemplos de JavaScript de la semana anterior, este HTML llega ya generado desde el servidor, como se confirma al ver el código fuente de la página. Se señala también un problema: si se visita la ruta sin pasar el parámetro name, el servidor responde con un error HTTP 400, por lo que se corrige el código en app.py usando una condición que verifica si name está en request.args, y si no lo está, asigna un valor por defecto como world.
El método get de request.args 25:01
Se muestra que es posible simplificar el código usando la función get del diccionario request.args, que permite pedir el parámetro name y, si no existe, asignar directamente un valor por defecto como world. Esto reduce cuatro líneas de lógica condicional a una sola línea, sin perder funcionalidad: si se escribe name igual a David en la URL, aparece el saludo correspondiente, y si no se escribe nada, aparece el valor por defecto.
Un formulario real y una segunda ruta 28:31
Se construye un formulario HTML en index.html con un campo de texto llamado name, marcador de posición, autocompletado desactivado, autofoco y un botón para enviar llamado saludar. La acción del formulario apunta a una nueva ruta llamada slash greet, que aún no existe en app.py. Se crea entonces esa ruta con su propia función, que devuelve una plantilla greet.html pasando el nombre obtenido de request.args.get. Al probarlo aparece un error interno del servidor porque falta crear el archivo greet.html, y se explica que este tipo de errores se ven en la terminal del desarrollador, no en el navegador del usuario, por razones de seguridad.
Duplicación entre plantillas 36:02
Una vez creado greet.html, se advierte que index.html y greet.html comparten casi todo el código HTML, salvo el contenido del cuerpo. Esta repetición se señala como un problema de diseño, comparable a la idea de abstracción vista en otros lenguajes: si hubiera tres, cuatro o cinco rutas, ese mismo HTML tendría que copiarse una y otra vez.
La plantilla base layout.html 37:01
Para resolver la duplicación se crea un tercer archivo, layout.html, que contiene todo el HTML común e invariante. Dentro del cuerpo se define un bloque dinámico usando la sintaxis de Jinja, la librería de plantillas de Flask, con llaves y signos de porcentaje en lugar de los corchetes angulares del HTML. Luego index.html y greet.html se reescriben para extender layout.html y definir únicamente su propio bloque body, eliminando todo el código repetido. El resultado visual en el navegador no cambia, aunque el HTML fuente ahora se genera combinando ambos archivos.
GET, POST y la privacidad de los datos 43:02
Se explica que el método GET coloca todos los parámetros directamente en la URL, lo cual es útil para crear enlaces directos pero problemático si se envían contraseñas, números de tarjeta o búsquedas privadas, ya que cualquiera con acceso al historial del navegador podría verlos. Se cambia entonces el formulario a método POST, lo que exige ajustar la ruta greet para aceptar methods igual a POST y reemplazar request.args por request.form. Se aclara que esta nomenclatura es confusa, ya que ambos provienen de formularios. Con las herramientas de desarrollador, en la pestaña Red y dentro de Carga útil, se puede verificar qué datos exactos se están enviando al servidor.
Unificar formulario y procesamiento en una ruta 48:00
Para evitar tener rutas separadas para mostrar el formulario y para procesarlo, se propone combinar ambas funciones en la ruta índice, aceptando tanto GET como POST. Dentro de esa única función se pregunta si el método de la solicitud es POST, en cuyo caso se procesa el formulario y se devuelve greet.html con el nombre correspondiente, y en caso contrario se asume que se debe mostrar simplemente el formulario mediante index.html.
Unificando las rutas de saludo 50:31
Al combinar la ruta de saludo con la ruta principal, el formulario de index.html ya no puede enviar los datos a /greet, porque esa ruta deja de existir. La solución es quitar esa referencia en el atributo action del formulario, o dejarlo vacío, de modo que el envío se dirija automáticamente a la misma URL desde la que se cargó la página. Se muestra además un toque final en greet.html: en vez de depender siempre del valor por defecto "world" que se pasaba desde app.py, se usa una sintaxis de Jinja parecida a Python (if, else, endif) para decidir dentro de la propia plantilla si se muestra el nombre recibido o, si viene vacío, la palabra "world". Se aclara también por qué el espacio en blanco extra alrededor de esas instrucciones no afecta el resultado: HTML colapsa cualquier espacio en blanco superfluo a uno solo, tal como había ocurrido la semana anterior con los párrafos del pato.
El origen real de este ejemplo 54:00
Se cuenta una anécdota personal: cuando el propio profesor estudiaba, no existía programación web en el curso y la web apenas comenzaba a existir. En esa época se involucró en el programa deportivo intramural para estudiantes de primer año, conocido como Frosh IMs, donde inscribirse en un deporte significaba llenar una hoja de papel y deslizarla bajo la puerta del encargado del dormitorio. Motivado por eso, construyó un sitio web usando Perl y archivos CSV que permitía inscribirse escribiendo el nombre y seleccionando un deporte, evitando así cruzar el campus con papel en mano. Este ejemplo histórico sirve como punto de partida para reconstruir, en clase, una versión moderna y simplificada de ese mismo sitio, dejando de lado adornos visuales anticuados como fondos repetidos.
Construyendo el formulario de inscripción 56:00
Se crea un nuevo proyecto llamado froshims con app.py, requirements.txt y una carpeta templates que incluye layout.html y un index.html vacío por completarse. En layout.html se añade una etiqueta especial en el encabezado que ayuda a que la página se redimensione correctamente en dispositivos móviles, algo relevante para el proyecto final. Luego se construye el formulario en index.html: un título con la etiqueta H1, un campo de texto para el nombre con marcador de posición, autocompletado desactivado y enfoque automático, y un menú desplegable (etiqueta select) con las opciones baloncesto, fútbol y ultimate frisbee, cada una con su propio atributo value. El formulario usa el método POST y envía los datos a la ruta /register.
Ajustes de usabilidad y primer error 1:01:00
Se corrige un detalle de diseño: en vez de que baloncesto aparezca seleccionado por defecto, se agrega una opción en blanco llamada "deporte" marcada como selected, para evitar que alguien se inscriba accidentalmente sin elegir nada. Se añade también un botón de tipo submit con la etiqueta "Registrar". Al probar el formulario aparece un error 404, porque la ruta /register todavía no existe en app.py. Se soluciona definiendo esa ruta con el método POST y una función register, que por ahora se limita a comprobar, usando request.form.get, si el usuario proporcionó tanto un nombre como un deporte; si falta alguno, se muestra una plantilla failure.html, y si todo está presente, se muestra success.html. Un error de sintaxis (una comilla sin cerrar) genera un error interno del servidor, que se detecta revisando el mensaje que Flask imprime en la terminal.
El peligro de confiar en el cliente 1:08:00
Se demuestra una vulnerabilidad real: usando las herramientas de desarrollador del navegador, es posible editar el HTML de la página y añadir una opción de voleibol que no estaba en el menú original, permitiendo inscribirse en un deporte que el sistema no ofrece. La lección es que nunca se debe confiar en la entrada del usuario, igual que se advirtió antes con SQL. La primera solución, comparar el deporte recibido contra cada nombre válido escrito a mano en app.py, funciona pero obliga a duplicar la lista de deportes tanto en el servidor como en la plantilla. La solución mejor es crear una variable global en mayúsculas, SPORTS, con la lista de deportes válidos, comprobar en el servidor si el valor recibido está en esa lista, y pasar esa misma lista a la plantilla para generar las opciones dinámicamente con un bucle for de Jinja. Así, con un solo lugar centralizado para gestionar los deportes, la inscripción falsa a voleibol vuelve a ser bloqueada, y agregar o quitar deportes solo requiere modificar esa lista una vez.
Botones de radio como alternativa al menú 1:15:00
David muestra otra forma de recoger la elección de deporte usando botones de radio en lugar de un menú desplegable. Genera dinámicamente una entrada de tipo radio para cada deporte, todas con el mismo atributo name, sport, lo que le indica al navegador que esas opciones son mutuamente excluyentes. El resultado visual no es elegante, pero funciona igual que antes: el servidor sigue recibiendo el nombre y el deporte dentro de request.form sin importar qué tipo de control haya usado el usuario en el navegador.
Una plantilla de error más clara 1:17:02
Como el sitio solo respondía con un mensaje genérico de no estás registrado cuando faltaba algún dato, David crea una plantilla error.html que extiende layout.html y muestra un mensaje concreto. En app.py valida por separado si falta el nombre, si falta el deporte, o si el deporte enviado no está en la lista permitida, y en cada caso devuelve render_template de error.html pasando un mensaje distinto como missing name, missing sport o invalid sport. Tras corregir un error tonto de escribir body block en vez de block body, el formulario empieza a mostrar explicaciones precisas en lugar de un fallo genérico.
Guardar los registros en memoria 1:21:30
David recuerda que originalmente el sitio real de Frosh IMs solo enviaba un correo al encargado con el nombre y el deporte de cada inscrito, hasta que se sustituyó por un almacenamiento real en el servidor. Para imitar eso, crea una variable global registrants como diccionario vacío y, en la ruta de registro, guarda cada nombre como clave con el deporte como valor. Luego añade una ruta nueva, /registrants, que renderiza una plantilla registrants.html con una tabla HTML generada con Jinja, recorriendo el diccionario para crear una fila por cada persona inscrita, igual que Google o Outlook generan filas para cada correo en una bandeja de entrada.
Enlaces y redirecciones entre páginas 1:27:00
Para no depender de que el usuario escriba la URL a mano, David añade en success.html un enlace HTML hacia /registrants para ver quién más se ha inscrito, y comenta que si se quisiera restringir esa página a personas concretas haría falta una contraseña, algo que se abordará más adelante con una página de inicio de sesión. Después mejora la experiencia importando la función redirect de Flask, de modo que tras registrarse el usuario es enviado automáticamente a la lista de inscritos en vez de ver la página de éxito.
El problema de la memoria volátil 1:30:00
David muestra que si el servidor Flask se detiene o se reinicia, la variable global registrants desaparece porque vivía solo en la memoria RAM, y pone como ejemplo que al reiniciar el servidor la lista de inscritos volvió a estar vacía. La solución es abandonar el diccionario global y usar una base de datos real: importa la función SQL de la librería de CS50, crea una conexión a un archivo froshims.db con una tabla registrants que tiene columnas id, name y sport, e inserta cada inscripción usando marcadores de posición en lugar de f-strings para evitar ataques de inyección SQL. Ajusta también la plantilla registrants.html porque ahora recibe una lista de diccionarios en vez de un solo diccionario, y comprueba que al detener y volver a arrancar el servidor los datos de David, Kelly, John y Doug permanecen guardados.
Preparando la baja de un inscrito 1:38:01
David plantea el caso de querer dar de baja a alguien, como Kelly, de la tabla de inscritos, y discute con la clase que lo que debe enviarse al servidor no es el nombre sino el id único de esa persona, ya que el nombre podría repetirse entre varios usuarios. Revisa con SELECT * FROM registrants en SQLite3 que cada fila tiene ese id como clave primaria, y comienza a diseñar en registrants.html una tercera columna con un formulario por cada fila, cuya acción apunta a una ruta /deregister mediante POST, con un botón de tipo submit llamado deregister, dejando pendiente decidir cómo incluir el id en ese formulario.
Eliminar registrados con ID oculta 1:40:32
David modifica el formulario de la tabla de registrados para que cada botón lleve, en un campo oculto, la ID real de esa persona en lugar de dejar que el usuario la escriba. Al ver el código fuente de la página comprueba que cada fila trae su propio ID oculto, por ejemplo 1 para él mismo y 2 para Kelly, y corrige un error donde había escrito el nombre de la variable en plural dentro del bucle for cuando debía llamarse registrant en singular.
Ruta para dar de baja 1:42:30
Crea una nueva ruta /deregister que toma la ID enviada por el formulario y, si existe, ejecuta un DELETE FROM registrants WHERE id igual al signo de interrogación, usando un marcador de posición para pasar la ID de forma segura. Al probarlo aparece un error de método no permitido, porque el formulario usa POST pero la ruta seguía aceptando GET por defecto, así que corrige el argumento methods para que acepte solo POST. Tras el cambio, dar de baja a Kelly y a sí mismo funciona y lo confirma consultando la base de datos desde la terminal.
Por qué usar POST y no GET 1:45:00
Explica que usar POST en lugar de GET para dar de baja a alguien no es solo para ocultar la ID en la URL, sino para evitar que una acción destructiva ocurra con solo visitar un enlace. Si la ruta aceptara GET, un adversario podría enviar por correo una URL como /registrants?id=4 y, si el supervisor del programa hace clic ingenuamente, Doug quedaría dado de baja sin querer, lo que abre la puerta a ataques de phishing. Por eso las solicitudes GET, que son simplemente URLs visitables, no deberían cambiar datos en el servidor, y las solicitudes POST, que requieren hacer clic en un botón dentro de una página, ofrecen una capa extra de protección.
Imágenes y la carpeta static 1:47:01
Para mostrar un gato gruñón junto al mensaje de error, intenta poner una etiqueta de imagen apuntando a cat.jpg en la carpeta actual, pero la imagen aparece rota. Descubre que Flask exige que cualquier archivo estático, como imágenes, CSS o JavaScript, viva dentro de una carpeta llamada static, a diferencia de las plantillas, que render_template encuentra automáticamente en la carpeta templates sin necesidad de mencionarla. Tras mover el archivo a static y actualizar la ruta en el HTML a /static/cat.jpg, la imagen se muestra correctamente.
De botones de opción a casillas 1:50:32
Para permitir que alguien se registre en varios deportes a la vez, cambia los botones de opción por casillas de verificación en el formulario, y ajusta la lógica en app.py usando request.form.getlist para obtener la lista completa de deportes marcados. Añade un bucle que verifica que cada deporte enviado sea válido y otro que inserta una fila por cada deporte elegido, aunque nota que esto duplica el nombre de la persona en la tabla y que un mejor diseño incluiría una tabla separada de estudiantes con su propio ID, similar a lo visto con la base de datos de IMDb.
El paradigma modelo vista controlador 1:55:01
Resume la arquitectura que ha ido construyendo con el término MVC: la vista es la interfaz que ve el usuario, es decir las plantillas; el controlador es la lógica en app.py que genera esas vistas; y el modelo es donde se guardan los datos persistentes, que pasó de ser un diccionario simple en memoria a la base de datos froshims.db. Aclara que la línea entre estas piezas no es estricta, porque las vistas también contienen variables, bucles y condicionales, pero esta forma de pensar es común en el desarrollo de aplicaciones web reales.
Cookies y cómo recuerdan quién eres 1:58:32
Explica que cuando alguien inicia sesión en un sitio como Gmail, el servidor responde con una cabecera set-cookie que coloca un par clave valor, normalmente una cadena aleatoria, en el navegador del usuario. Esa cookie funciona como un sello de mano en un club o parque de atracciones: se presenta una y otra vez en cada solicitud para que el servidor sepa que ya se verificó la identidad antes, sin tener que volver a pedir usuario y contraseña. Advierte que guardar contraseñas directamente en cookies es mal diseño, y que el modo incógnito borra estas cookies al cerrar la ventana, lo que explica por qué los anunciantes pueden reconocer a un mismo usuario en varios sitios distintos.
Sesiones y la librería Flask-Session 2:02:01
Introduce el concepto de sesión como un diccionario de pares clave valor asociado a cada usuario, que el servidor mantiene en memoria gracias a la presentación repetida de la cookie. Presenta la biblioteca de terceros Flask-Session, que permite usar cookies sin tener que entender manualmente las cabeceras HTTP, y abre un nuevo proyecto llamado login con un formulario simple que solo pide un nombre de usuario, sin contraseña, para mostrar cómo funciona el inicio de sesión.
Sesiones para iniciar y cerrar sesión 2:05:01
El profesor muestra cómo Flask permite que un sitio recuerde quién ha iniciado sesión mediante el objeto session, que funciona como un diccionario propio para cada usuario. En app.py se importa Session desde flask_session y se configura la aplicación para guardar las cookies como archivos en el servidor. La ruta principal renderiza index.html pasando el nombre guardado en session.get, y la plantilla decide con lógica condicional si muestra un enlace para iniciar sesión o uno para cerrar sesión, según haya o no un nombre almacenado.
Cómo entran y salen los usuarios 2:08:31
En la ruta de inicio de sesión, si la petición llega por POST, el nombre escrito en el formulario se guarda en session con la clave name y el usuario es redirigido a la página principal. Si la petición es GET, simplemente se muestra login.html, un formulario simple con un cuadro de texto y un botón de enviar. Para cerrar sesión basta con llamar a session.clear, lo que borra los datos del lado del servidor asociados a la cookie del usuario, de forma similar a como funcionan los inicios de sesión en sitios como Facebook, Gmail u Outlook, aunque estos añaden contraseñas y otras medidas de seguridad.
Carrito de compras con sesiones 2:09:00
Como segundo ejemplo se implementa una tienda de libros sencilla, con una base de datos store.db que contiene una tabla de libros, entre ellos La guía del autoestopista galáctico y sus secuelas. Cada libro se muestra con un formulario oculto que envía su ID por POST a la ruta /cart. En el servidor, si no existe un carrito en la sesión se crea una lista vacía, y cada vez que se agrega un libro su ID se añade a esa lista; luego se redirige al usuario a la misma ruta, esta vez por GET, para mostrar con una consulta SQL con IN los libros correspondientes a esos IDs. Así, cada usuario ve solo su propio carrito, igual que ocurre en Amazon, donde cada persona tiene su copia separada de la sesión.
Buscador de programas de televisión 2:18:32
El siguiente ejemplo usa la base de datos shows.db, la misma de semanas anteriores con datos de IMDb, para construir un buscador simple. Una primera versión busca coincidencias exactas con el título, por lo que escribir the office en minúsculas no da resultados. Una segunda versión usa la palabra clave LIKE junto con el símbolo de porcentaje como comodín antes y después del texto buscado, lo que permite encontrar cualquier programa cuyo título contenga esa cadena, sin importar mayúsculas ni minúsculas ni su posición.
De HTML a JSON: construir una API 2:20:32
Las últimas versiones del ejemplo muestran cómo hacer el buscador interactivo, enviando una solicitud a la ruta /search cada vez que se escribe una letra, gracias a JavaScript. En una versión, esa ruta devuelve solo fragmentos de HTML con etiquetas li; en otra, devuelve los datos en formato JSON, la notación de objetos de JavaScript, similar a listas y diccionarios de Python, usando la función jsonify de Flask. Esto ilustra el concepto de una API, una interfaz de programación de aplicaciones que permite que terceros obtengan datos de un servicio mediante HTTP, algo comparable a cómo CS50 se comunicaba antes con la API de OpenAI. Con esto se cierra el repaso de programación web, dejando listas las bases para construir aplicaciones web o móviles propias en el proyecto final.
AI-generated summary. It can be wrong or incomplete - check anything that matters against the original.

