Índice

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:

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

  1. Marcar como leído: Agrega una ruta /leido/N que marque el libro N como leído y redirija a /libros.
  1. Eliminar libros: Agrega una ruta /eliminar/N que quite el libro N de la lista.
  1. Búsqueda: Agrega una ruta /buscar?q=termino que muestre solo libros que coincidan con el término de búsqueda.
  1. Persistencia: Guarda los libros en un archivo (libros.csv) en cada cambio y cárgalos al iniciar.
  1. API JSON: Agrega /api/libros que devuelva la lista de libros como JSON, y /api/libros/agregar que 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 →

← Anterior: Concurrencia — Threads y channels Siguiente: LLVM y rendimiento →