Tu segundo proyecto — Un servidor web
Juntando la Parte 2
En la Parte 1, construiste un seguimiento de notas de estudiantes. Ahora conoces módulos, archivos, closures, enums, traits, generics, networking y concurrencia. Es momento de juntar todo y construir algo real: un servidor web multi-threaded que sirve páginas dinámicas.
Construiremos una aplicación de estantería de libros personal: los usuarios pueden ver libros, agregar nuevos y ver estadísticas — todo a través de un navegador web.
Paso 1: Definir los datos
struct Libro { titulo: String, autor: String, paginas: int, leido: bool } fn crear_libro(titulo: String, autor: String, paginas: int) -> Libro { return Libro { titulo: titulo, autor: autor, paginas: paginas, leido: false } }
Paso 2: Construir el almacén de libros
Guardaremos los libros en un array y proporcionaremos funciones para trabajar con ellos:
fn contar_leidos(libros: Array) -> int { var cuenta: int = 0 var i: int = 0 while i < libros.length() { let l: Libro = libros[i] if l.leido { cuenta += 1 } i += 1 } return cuenta } fn total_paginas(libros: Array) -> int { var total: int = 0 var i: int = 0 while i < libros.length() { let l: Libro = libros[i] total += l.paginas i += 1 } return total } fn encontrar_mas_largo(libros: Array) -> String { if libros.length() == 0 { return "Ninguno" } var mejor: String = "" var mejor_paginas: int = 0 var i: int = 0 while i < libros.length() { let l: Libro = libros[i] if l.paginas > mejor_paginas { mejor_paginas = l.paginas mejor = l.titulo } i += 1 } return mejor }
Paso 3: Construir páginas HTML
Creemos funciones que generen HTML:
fn pagina_html(titulo: String, cuerpo: String) -> String { return "<!DOCTYPE html><html><head><title>" + titulo + "</title>" + "<style>body{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px;}" + "h1{color:#333;}table{width:100%;border-collapse:collapse;}" + "th,td{padding:8px;text-align:left;border-bottom:1px solid #ddd;}" + "a{color:#0066cc;}a:hover{text-decoration:underline;}" + ".stat{background:#f5f5f5;padding:15px;margin:10px 0;border-radius:5px;}" + "</style></head><body>" + cuerpo + "</body></html>" } fn barra_nav() -> String { return "<nav><a href=\"/\">Inicio</a> | <a href=\"/libros\">Libros</a> | <a href=\"/stats\">Estadísticas</a></nav><hr>" }
Paso 4: La página de lista de libros
fn pagina_libros(libros: Array) -> String { var filas: String = "" var i: int = 0 while i < libros.length() { let l: Libro = libros[i] var estado: String = "No" if l.leido { estado = "Sí" } filas = filas + "<tr><td>" + l.titulo + "</td><td>" + l.autor + "</td><td>" + int_to_string(l.paginas) + "</td><td>" + estado + "</td></tr>" i += 1 } let cuerpo: String = barra_nav() + "<h1>Mi Estantería</h1>" + "<table><tr><th>Título</th><th>Autor</th><th>Páginas</th><th>¿Leído?</th></tr>" + filas + "</table>" + "<br><p><a href=\"/agregar\">+ Agregar un libro</a></p>" return pagina_html("Libros", cuerpo) }
Paso 5: La página de estadísticas
fn pagina_stats(libros: Array) -> String { let total: int = libros.length() let leidos: int = contar_leidos(libros) let no_leidos: int = total - leidos let paginas: int = total_paginas(libros) let mas_largo: String = encontrar_mas_largo(libros) let cuerpo: String = barra_nav() + "<h1>Estadísticas de Lectura</h1>" + "<div class=\"stat\">Total de libros: " + int_to_string(total) + "</div>" + "<div class=\"stat\">Leídos: " + int_to_string(leidos) + " / Sin leer: " + int_to_string(no_leidos) + "</div>" + "<div class=\"stat\">Total de páginas: " + int_to_string(paginas) + "</div>" + "<div class=\"stat\">Libro más largo: " + mas_largo + "</div>" return pagina_html("Estadísticas", cuerpo) }
Paso 6: El formulario para agregar libros
fn pagina_agregar() -> String { let cuerpo: String = barra_nav() + "<h1>Agregar un Libro</h1>" + "<form method=\"POST\" action=\"/agregar\">" + "<p>Título: <input name=\"titulo\" required></p>" + "<p>Autor: <input name=\"autor\" required></p>" + "<p>Páginas: <input name=\"paginas\" type=\"number\" required></p>" + "<p><button type=\"submit\">Agregar Libro</button></p>" + "</form>" return pagina_html("Agregar Libro", cuerpo) }
Paso 7: Parsear datos del formulario
Cuando el usuario envía el formulario, el navegador manda una solicitud POST con datos del formulario. Parseémoslos:
fn parsear_formulario(body: String) -> Map { var resultado: Map = Map.new() let pares: Array = body.split("&") var i: int = 0 while i < pares.length() { let par: String = pares[i] let eq: int = par.indexOf("=") if eq > 0 { let clave: String = par.substring(0, eq) let valor: String = par.substring(eq + 1, par.length()) resultado.insert(clave, valor) } i += 1 } return resultado }
Paso 8: El router de solicitudes
Ahora conectemos todo con un manejador de solicitudes.
El programa completo
struct Libro { titulo: String, autor: String, paginas: int, leido: bool } fn crear_libro(titulo: String, autor: String, paginas: int) -> Libro { return Libro { titulo: titulo, autor: autor, paginas: paginas, leido: false } } fn contar_leidos(libros: Array) -> int { var cuenta: int = 0 var i: int = 0 while i < libros.length() { let l: Libro = libros[i] if l.leido { cuenta += 1 } i += 1 } return cuenta } fn total_paginas(libros: Array) -> int { var total: int = 0 var i: int = 0 while i < libros.length() { let l: Libro = libros[i] total += l.paginas i += 1 } return total } fn encontrar_mas_largo(libros: Array) -> String { if libros.length() == 0 { return "Ninguno" } var mejor: String = "" var mejor_paginas: int = 0 var i: int = 0 while i < libros.length() { let l: Libro = libros[i] if l.paginas > mejor_paginas { mejor_paginas = l.paginas mejor = l.titulo } i += 1 } return mejor } fn pagina_html(titulo: String, cuerpo: String) -> String { return "<!DOCTYPE html><html><head><title>" + titulo + "</title>" + "<style>body{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px;}" + "h1{color:#333;}table{width:100%;border-collapse:collapse;}" + "th,td{padding:8px;text-align:left;border-bottom:1px solid #ddd;}" + "a{color:#0066cc;}a:hover{text-decoration:underline;}" + ".stat{background:#f5f5f5;padding:15px;margin:10px 0;border-radius:5px;}" + "</style></head><body>" + cuerpo + "</body></html>" } fn barra_nav() -> String { return "<nav><a href=\"/\">Inicio</a> | <a href=\"/libros\">Libros</a> | <a href=\"/stats\">Estadísticas</a></nav><hr>" } fn pagina_libros(lbs: Array) -> String { var filas: String = "" var i: int = 0 while i < lbs.length() { let l: Libro = lbs[i] var estado: String = "No" if l.leido { estado = "Sí" } filas = filas + "<tr><td>" + l.titulo + "</td><td>" + l.autor + "</td><td>" + int_to_string(l.paginas) + "</td><td>" + estado + "</td></tr>" i += 1 } let cuerpo: String = barra_nav() + "<h1>Mi Estantería</h1>" + "<table><tr><th>Título</th><th>Autor</th><th>Páginas</th><th>¿Leído?</th></tr>" + filas + "</table><br><p><a href=\"/agregar\">+ Agregar un libro</a></p>" return pagina_html("Libros", cuerpo) } fn pagina_stats(lbs: Array) -> String { let total: int = lbs.length() let leidos: int = contar_leidos(lbs) let paginas: int = total_paginas(lbs) let mas_largo: String = encontrar_mas_largo(lbs) let cuerpo: String = barra_nav() + "<h1>Estadísticas de Lectura</h1>" + "<div class=\"stat\">Total de libros: " + int_to_string(total) + "</div>" + "<div class=\"stat\">Leídos: " + int_to_string(leidos) + " / Sin leer: " + int_to_string(total - leidos) + "</div>" + "<div class=\"stat\">Total de páginas: " + int_to_string(paginas) + "</div>" + "<div class=\"stat\">Libro más largo: " + mas_largo + "</div>" return pagina_html("Estadísticas", cuerpo) } fn pagina_agregar() -> String { let cuerpo: String = barra_nav() + "<h1>Agregar un Libro</h1>" + "<form method=\"POST\" action=\"/agregar\">" + "<p>Título: <input name=\"titulo\" required></p>" + "<p>Autor: <input name=\"autor\" required></p>" + "<p>Páginas: <input name=\"paginas\" type=\"number\" required></p>" + "<p><button type=\"submit\">Agregar Libro</button></p></form>" return pagina_html("Agregar Libro", cuerpo) } fn parsear_formulario(body: String) -> Map { var resultado: Map = Map.new() let pares: Array = body.split("&") var i: int = 0 while i < pares.length() { let par: String = pares[i] let eq: int = par.indexOf("=") if eq > 0 { let clave: String = par.substring(0, eq) let valor: String = par.substring(eq + 1, par.length()) resultado.insert(clave, valor) } i += 1 } return resultado } import { http_serve_mt, http_response, http_response_with_headers } from "std/http" var libros: Array = [] fn al_recibir_solicitud(request: Array) -> String { let metodo: String = request[1] let ruta: String = request[2] let body: String = request[4] let html_headers: Array = ["Content-Type", "text/html; charset=utf-8"] if ruta == "/" { let inicio: String = barra_nav() + "<h1>Bienvenido a Mi Estantería</h1>" + "<p>Un registro personal de libros construido con Nyx.</p>" + "<p>Tienes <strong>" + int_to_string(libros.length()) + "</strong> libros.</p>" + "<p><a href=\"/libros\">Ver todos los libros</a></p>" return http_response_with_headers(200, html_headers, pagina_html("Inicio", inicio)) } if ruta == "/libros" { return http_response_with_headers(200, html_headers, pagina_libros(libros)) } if ruta == "/stats" { return http_response_with_headers(200, html_headers, pagina_stats(libros)) } if ruta == "/agregar" and metodo == "GET" { return http_response_with_headers(200, html_headers, pagina_agregar()) } if ruta == "/agregar" and metodo == "POST" { let form: Map = parsear_formulario(body) if form.contains("titulo") and form.contains("autor") and form.contains("paginas") { libros.push(crear_libro(form.get("titulo"), form.get("autor"), string_to_int(form.get("paginas")))) } let redir: Array = ["Location", "/libros"] return http_response_with_headers(302, redir, "") } return http_response(404, "Página no encontrada") } fn main() { // Datos de ejemplo libros.push(crear_libro("El Programador Pragmático", "Hunt y Thomas", 352)) libros.push(crear_libro("Estructura e Interpretación", "Abelson y Sussman", 657)) libros.push(crear_libro("El Lenguaje C", "Kernighan y Ritchie", 288)) print("Servidor de estantería en http://localhost:8080") print("Abre tu navegador y visita http://localhost:8080") http_serve_mt(8080, 4, al_recibir_solicitud) }
Lo que construiste
Este programa usa todo lo de la Parte 2:
- Módulos — importando desde
std/http. - Structs —
Libropara modelar datos. - Arrays — almacenando la colección de libros.
- Maps — parseando datos del formulario.
- Strings — construyendo HTML dinámicamente.
- Funciones — cada página y utilidad es su propia función.
- Flujo de control — enrutando solicitudes por ruta y método.
- Concurrencia —
http_serve_mtmaneja múltiples clientes con threads.
Abre http://localhost:8080 en tu navegador y tienes una aplicación web funcionando — construida enteramente en Nyx, compilada a código nativo, ejecutándose a miles de solicitudes por segundo.
Desafíos
- Marcar como leído: Agrega una ruta
/leido/Nque marque el libro N como leído y redirija a/libros.
- Eliminar libros: Agrega una ruta
/eliminar/Nque quite el libro N de la lista.
- Búsqueda: Agrega una ruta
/buscar?q=terminoque muestre solo libros que coincidan con el término de búsqueda.
- Persistencia: Guarda los libros en un archivo (
libros.csv) en cada cambio y cárgalos al iniciar.
- API JSON: Agrega
/api/librosque devuelva la lista de libros como JSON, y/api/libros/agregarque acepte solicitudes POST con JSON.
¿Qué sigue?
¡Felicitaciones! Has completado la Parte 2 de El Libro de Nyx. Ahora puedes construir aplicaciones reales conectadas en red: módulos para organización, archivos para persistencia, closures para flexibilidad, enums para seguridad, traits para abstracción, generics para reutilización, networking para comunicación y concurrencia para rendimiento.
En la Parte 3, irás más profundo: internos de LLVM, FFI para llamar código C, operaciones unsafe, I/O asíncrono, y un caso de estudio de cómo nyx-kv (una base de datos compatible con Redis) fue construida enteramente en Nyx.
Siguiente capítulo: LLVM y rendimiento →