El módulo 9 cerró con una constatación incómoda: Papyrus funciona, está probado y tiene red de seguridad, pero vive en un solo ordenador. Si Luis quiere saber cuántos ejemplares de Fausto quedan, tiene que llamar a Ana; si Júlia quiere reservar Hamlet, tiene que acercarse a la tienda. La solución tiene nombre desde hace treinta años: convertir Papyrus en una aplicación web, un programa que corre en un servidor y al que cualquiera accede desde un navegador o un móvil. Antes de escribir la primera línea de Flask (eso llega en la próxima lección), necesitas entender el terreno de juego: cómo conversan un navegador y un servidor, qué es HTTP, qué pinta el HTML en todo esto y qué te regala un framework. Esta lección es el mapa; las cuatro siguientes, el viaje.

Contenido

  1. El modelo cliente-servidor: quién pide y quién responde
  2. HTTP: el protocolo de la conversación
  3. Métodos HTTP: los verbos de la web
  4. Códigos de estado: el semáforo de las respuestas
  5. Anatomía de una URL
  6. HTML: lo mínimo imprescindible para este módulo
  7. Frontend y backend: dos mundos, una frontera
  8. JSON: el idioma de las APIs (viejo conocido)
  9. Qué hace un framework por ti
  10. El panorama Python: Flask, Django y FastAPI

El modelo cliente-servidor: quién pide y quién responde

Toda la web se sostiene sobre una idea simple: hay programas que piden (clientes) y programas que responden (servidores). El navegador de Luis es un cliente; el ordenador donde correrá Papyrus será un servidor. La conversación siempre la inicia el cliente — el servidor nunca llama a nadie por iniciativa propia, solo espera peticiones y las contesta.

sequenceDiagram
    participant N as Navegador de Luis (cliente)
    participant S as Servidor de Papyrus
    N->>S: GET /catalogo  (petición HTTP)
    Note over S: Ejecuta código Python:<br/>cargar_catalogo(), montar la página
    S->>N: 200 OK + HTML del catálogo (respuesta)
    Note over N: El navegador dibuja la página
    N->>S: GET /libro/Fausto
    S->>N: 200 OK + ficha de Fausto (21.00 €, stock 10)

Fíjate en el reparto de papeles:

  • El cliente (navegador, app móvil, o incluso un script Python con requests) construye una petición, la envía y espera.
  • El servidor recibe la petición, ejecuta código — en nuestro caso, código Python que ya tienes escrito: buscar_libro(), vender(), cargar_catalogo() — y devuelve una respuesta.
  • Cada intercambio es independiente: petición → respuesta, y la conexión conceptualmente termina. Esta amnesia del servidor (se llama sin estado, y volveremos a ella en 10-03) es lo que permite que miles de clientes hablen con el mismo servidor sin que este tenga que recordar a cada uno.

La consecuencia práctica para Papyrus: el paquete papyrus/ que construiste en M3–M9 no cambia. Lo que vas a añadir es una capa por encima que traduce peticiones HTTP en llamadas a tus funciones, y los valores de retorno (o las excepciones) en respuestas HTTP.

HTTP: el protocolo de la conversación

HTTP (HyperText Transfer Protocol) es el formato acordado de esas peticiones y respuestas. Es texto, y es legible. Una petición real tiene tres partes — línea inicial, cabeceras y cuerpo opcional:

GET /libro/Hamlet HTTP/1.1
Host: papyrus.example
Accept: text/html

Y la respuesta, la misma estructura:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1274

<!DOCTYPE html>
<html>...la ficha de Hamlet...</html>
  • La línea inicial de la petición dice el método (GET), la ruta (/libro/Hamlet) y la versión del protocolo.
  • Las cabeceras (headers) son metadatos en formato Clave: valor — qué formato acepta el cliente, qué formato envía el servidor, cuánto ocupa el cuerpo...
  • El cuerpo (body) lleva el contenido: HTML en una página web, JSON en una API, nada en muchas peticiones GET.

No necesitas escribir HTTP a mano jamás — el navegador y el framework lo hacen por ti —, pero saber leerlo te convertirá en alguien que depura problemas web en minutos en lugar de horas.

Métodos HTTP: los verbos de la web

El método declara la intención de la petición. Cuatro cubren casi todo, y encajan de forma natural con las operaciones que Papyrus ya sabe hacer:

Método Intención Ejemplo en Papyrus ¿Modifica datos?
GET Leer, consultar Ver el catálogo, buscar La Odisea No
POST Crear algo nuevo Dar de alta un libro nuevo en el catálogo
PUT Actualizar algo existente Cambiar el stock de Hamlet de 6 a 9
DELETE Eliminar Retirar un libro descatalogado

Dos reglas de oro que los frameworks respetan y tú también deberás respetar:

  • GET nunca debe modificar datos. Un buscador que indexe tu web seguirá todos los enlaces GET que encuentre; si /vender/Hamlet fuera un GET, un robot podría vaciarte el stock sin querer.
  • POST, PUT y DELETE llevan la información sensible en el cuerpo de la petición, no en la URL (las URLs acaban en logs e historiales).

Códigos de estado: el semáforo de las respuestas

Cada respuesta HTTP empieza con un código de tres cifras que resume cómo fue la cosa. La primera cifra es la familia: 2xx éxito, 4xx el cliente se equivocó, 5xx el servidor se rompió. Estos cinco son los que usarás constantemente:

Código Nombre Cuándo lo devolverá Papyrus
200 OK Éxito GET /libro/Hamlet y Hamlet existe: aquí tienes su ficha
201 Created Creado POST de un libro nuevo aceptado: Fausto II ya está en el catálogo
400 Bad Request Petición mal formada POST de un libro sin titulo o con precio: "gratis"
404 Not Found No existe GET /libro/Rayuela — no lo tenemos (¿te suena LibroNoEncontradoError?)
500 Internal Server Error Error del servidor Una excepción no capturada en nuestro código Python

La fila del 404 es un adelanto importante: en M7 diseñaste LibroNoEncontradoError para señalar exactamente esta situación dentro de Python. En 10-03 verás que la traducción excepción → código de estado es literal: capturas LibroNoEncontradoError y respondes 404. Tu jerarquía de excepciones de M7 ya estaba pensando en la web sin saberlo.

Y un matiz de actitud profesional: un 500 siempre es culpa tuya (o de tu código). Si un usuario puede provocar un 500 escribiendo algo raro en un formulario, te falta validación — debería ser un 400 con un mensaje útil.

Anatomía de una URL

La URL es la dirección completa de un recurso. Diseccionemos una futura URL de Papyrus:

https://papyrus.example:443/libros/buscar?titulo=odisea&max=10#resultados
└─┬─┘   └──────┬───────┘└┬┘└─────┬──────┘└──────────┬─────────┘└───┬────┘
esquema      host       puerto   ruta          query string     fragmento
  • Esquema: el protocolo, http o https (la s es HTTP cifrado; hoy, siempre https en producción).
  • Host: el nombre del servidor. En desarrollo será localhost o 127.0.0.1 — tu propia máquina.
  • Puerto: la "puerta" del servidor. Se omite si es el estándar (80 para http, 443 para https). El servidor de desarrollo de Flask usa el 5000 y el de Django el 8000; los verás mil veces.
  • Ruta (path): qué recurso se pide. Es lo que tus rutas de Flask/Django van a mapear a funciones.
  • Query string: parámetros opcionales tras ?, en pares clave=valor separados por &. Es donde viajan los términos de búsqueda de un formulario GET.
  • Fragmento: tras #, una posición dentro de la página; no llega al servidor.

HTML: lo mínimo imprescindible para este módulo

Seamos honestos: esto no es un curso de HTML, y no intentará serlo. Pero las plantillas de Flask y Django generan HTML, así que necesitas leerlo y escribir el básico. HTML es un lenguaje de marcado: envuelves contenido entre etiquetas que declaran qué es cada cosa.

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <title>Papyrus — Librería</title>
</head>
<body>
    <h1>Catálogo de Papyrus</h1>
    <p>Los clásicos de siempre, en tu barrio.</p>
    <ul>
        <li>La Odisea — 12.50 €</li>
        <li>Hamlet — 9.95 €</li>
    </ul>
    <a href="/libro/Fausto">Ver ficha de Fausto</a>
</body>
</html>

Línea a línea: <!DOCTYPE html> declara HTML moderno; <head> contiene metadatos que no se pintan (codificación, título de la pestaña); <body> es lo visible; <h1> un encabezado, <p> un párrafo, <ul>/<li> una lista, <a href="..."> un enlace. Las etiquetas se abren <p> y se cierran </p>.

La pieza que más usarás es el formulario, porque es como el usuario envía datos al servidor:

<form action="/buscar" method="get">
    <label for="titulo">Buscar libro:</label>
    <input type="text" id="titulo" name="titulo">
    <button type="submit">Buscar</button>
</form>
  • action es la ruta a la que se envía; method el verbo HTTP (get para búsquedas, post para crear/modificar).
  • Cada <input> con name se convierte en un parámetro: al buscar "odisea", el navegador pide GET /buscar?titulo=odisea. Ahí está la query string de antes.
  • <button type="submit"> dispara el envío.

Con encabezados, párrafos, listas, tablas (<table>, <tr>, <td>), enlaces y formularios tienes el 95 % de lo que este módulo necesita.

Frontend y backend: dos mundos, una frontera

Frontend Backend
Dónde se ejecuta En el navegador del usuario En el servidor
Lenguajes típicos HTML, CSS, JavaScript Python (¡lo nuestro!), y muchos otros
Responsabilidad Presentación e interacción Lógica de negocio, datos, seguridad
En Papyrus Las páginas que verán Luis y Júlia buscar_libro(), vender(), el catálogo

La frontera entre ambos es exactamente HTTP: el frontend pide, el backend responde. Este curso es de Python, así que viviremos en el backend; el frontend que escribiremos será HTML modesto generado por plantillas, suficiente para que Papyrus sea usable. Una regla que nunca debes olvidar: toda validación del frontend es cortesía, no seguridad. Cualquiera puede enviar peticiones HTTP sin pasar por tu formulario (lo harás tú mismo con curl en 10-03), así que el backend debe validar siempre, se fíe o no de la página que sirvió.

JSON: el idioma de las APIs (viejo conocido)

Cuando el cliente no es un navegador que quiere HTML sino un programa que quiere datos — la futura app móvil de Júlia, un script de Luis —, la respuesta viaja en JSON. Y aquí no hay nada nuevo que aprender: es exactamente el formato de M6, el mismo de datos/catalogo.json:

{"titulo": "La Odisea", "precio": 12.5, "stock": 4}

json.dumps() convertía diccionarios en texto JSON; asdict() (M5) convertía tus dataclasses Libro en diccionarios. La cadena Libro → asdict() → JSON → respuesta HTTP es la columna vertebral de la lección 10-03, y ya conoces dos de sus tres eslabones. Una API (Application Programming Interface) web no es más que un conjunto de URLs pensadas para programas: mismas peticiones HTTP, misma estructura, pero JSON en lugar de HTML en el cuerpo.

Qué hace un framework por ti

Podrías escribir un servidor web con la librería estándar de Python (http.server existe), igual que podrías escribir tus propios tests sin pytest. Nadie lo hace, por la misma razón: un framework web resuelve de serie los problemas que toda aplicación web comparte.

  • Enrutado: mapear GET /libro/Hamlet a tu función Python que sabe responder. Sin framework, sería un if/elif gigante sobre la ruta.
  • Plantillas: generar HTML mezclando una estructura fija con datos variables (el catálogo, el resultado de una búsqueda), sin concatenar cadenas a mano.
  • Formularios: leer los datos enviados, convertir tipos, validar.
  • Seguridad: protección contra ataques clásicos (inyección de HTML malicioso, falsificación de peticiones) que ni siquiera sabías que existían — y de las que hablaremos en 10-05.
  • Errores: convertir excepciones en respuestas 404/500 razonables en lugar de dejar caer el servidor.

El framework se ocupa de la fontanería HTTP; tú escribes solo lo que hace única a tu aplicación. En Papyrus, esa parte única ya está escrita y probada: es el paquete papyrus/.

El panorama Python: Flask, Django y FastAPI

Python tiene tres frameworks dominantes, y este módulo te enseñará los dos primeros:

Flask Django FastAPI
Filosofía Micro: núcleo mínimo, añades lo que necesitas Baterías incluidas: trae de todo Moderno, especializado en APIs
Curva de aprendizaje Suave: app en 5 líneas Más empinada: estructura y convenciones Suave si dominas type hints (M8)
Trae de serie Rutas, plantillas, servidor de desarrollo Además: ORM (base de datos), admin, formularios, usuarios Validación automática, documentación automática
Brilla en Proyectos pequeños/medianos, APIs, aprender Aplicaciones grandes con muchas piezas estándar APIs de alto rendimiento (async, M8)
En este módulo 10-02 y 10-03 10-04 y 10-05 Solo esta mención honesta

Los tres se instalan como cualquier paquete: pip install flask, pip install django — dentro de un entorno virtual, como aprendiste en M1. FastAPI merece la mención porque es el tercero en discordia y usa intensivamente los type hints de M8 y asyncio para servidores asíncronos; si algún día construyes una API seria, míralo. Pero para aprender los fundamentos, Flask primero (verás cada pieza por separado) y Django después (verás lo que es tenerlas todas integradas) es el camino clásico, y es el nuestro.

Errores Comunes y Consejos

  • Confundir la URL con la página. La URL identifica el recurso; la página es la respuesta. Dos peticiones a la misma URL pueden devolver cosas distintas (el stock cambia). Piensa en URLs como preguntas, no como ficheros.
  • Usar GET para operaciones que modifican datos. Es el error conceptual más peligroso de los principiantes. Si modifica algo: POST, PUT o DELETE. Siempre.
  • Devolver 200 para todo. Una API que responde 200 OK con el cuerpo {"error": "no encontrado"} es una API mentirosa: los clientes automáticos miran primero el código. Códigos correctos desde el primer día.
  • Creer que hay que dominar HTML/CSS/JavaScript antes de seguir. No. Con el HTML de esta lección basta para todo el módulo. El backend es una especialidad completa por sí misma.
  • Olvidar que el servidor no recuerda nada entre peticiones. Cada petición llega huérfana. Los mecanismos para "recordar" usuarios (sesiones, cookies) existen y los frameworks los dan hechos, pero el protocolo base es amnésico — y eso es una virtud, como verás en 10-03.

Ejercicios

Ejercicio 1: verbos y códigos

Para cada operación de Papyrus, indica el método HTTP adecuado y el código de estado que debería devolver el servidor en el escenario descrito:

  1. Júlia consulta la ficha de El Quijote (existe).
  2. Ana da de alta Fausto II con título, precio y stock válidos.
  3. Luis consulta la ficha de Rayuela (no está en el catálogo).
  4. Un script envía un alta de libro con "precio": "veinte euros".
  5. Ana actualiza el stock de Hamlet a 9 unidades.

Ejercicio 2: disección de URL

Descompón en sus partes (esquema, host, puerto, ruta, query string) la URL:

http://localhost:5000/buscar?titulo=quijote&socio=true

¿Qué pareja clave=valor recibirá el servidor de un formulario cuyo único <input> tiene name="titulo" si el usuario escribe fausto?

Ejercicio 3: el formulario de reserva

Escribe el HTML de un formulario para que Júlia reserve un libro: un campo de texto para el título, un campo numérico (type="number") llamado unidades, y un botón "Reservar". La reserva modifica datos (aparta stock): elige action y method en consecuencia y justifica la elección en un comentario HTML (<!-- ... -->).

Soluciones

Ejercicio 1:

  1. GET200 OK (lectura con éxito).
  2. POST201 Created (recurso nuevo creado).
  3. GET404 Not Found (la petición está bien formada; el recurso no existe — el caso LibroNoEncontradoError).
  4. POST400 Bad Request (el cliente envió datos inválidos; validamos y explicamos el motivo, nunca dejamos que estalle en un 500).
  5. PUT200 OK (actualización de un recurso existente).

Ejercicio 2: esquema http, host localhost, puerto 5000 (el de desarrollo de Flask), ruta /buscar, query string titulo=quijote&socio=true (dos parámetros: titulo=quijote y socio=true). Con el input name="titulo" y el valor fausto, el servidor recibe titulo=fausto — la URL resultante sería /buscar?titulo=fausto.

Ejercicio 3:

<!-- method="post" porque reservar modifica el stock: las operaciones
     con efectos nunca deben viajar en GET (los robots siguen enlaces GET
     y los parámetros GET acaban en historiales y logs). -->
<form action="/reservar" method="post">
    <label for="titulo">Título:</label>
    <input type="text" id="titulo" name="titulo">
    <label for="unidades">Unidades:</label>
    <input type="number" id="unidades" name="unidades" min="1">
    <button type="submit">Reservar</button>
</form>

Conclusión

Ya tienes el mapa. La web es un diálogo de peticiones y respuestas HTTP entre clientes que piden y un servidor que ejecuta código — pronto, tu código. Los métodos (GET lee, POST crea, PUT actualiza, DELETE borra) declaran la intención; los códigos de estado (200, 201, 400, 404, 500) resumen el resultado, y ya viste que LibroNoEncontradoError y el 404 están destinados a encontrarse. Sabes leer una URL pieza a pieza, escribir el HTML mínimo con formularios incluidos, y distinguir el frontend (presentación, en el navegador) del backend (lógica y datos, en Python). Y sabes qué framework elegirás para cada cosa: Flask para empezar ligero, Django cuando quieras las baterías incluidas, FastAPI como asignatura optativa futura. En la próxima lección la teoría se acaba: instalarás Flask en el venv, escribirás una aplicación completa en cinco líneas, descubrirás que @app.route es exactamente uno de aquellos decoradores de 08-02, y pondrás el catálogo de Papyrus — La Odisea, Hamlet, El Quijote y Fausto — en un navegador por primera vez.

Curso de Programación en Python

Módulo 1: Introducción a Python

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados