Módulos
Cuatro piezas que se toman por separado. Si solo necesitas atender HTTP, lux-http basta y son 64 KB.
Los cinco módulos
Cada módulo se toma por separado. Si solo necesitas atender HTTP, lux-http
basta y son 64 KB.
| Módulo | Necesita | Clases | Líneas | Pruebas | Tamaño |
|---|---|---|---|---|---|
| lux-httpServidor HTTP/1.1, TLS recargable, sesiones enchufables, multipart, gzip, estáticos con rangos, WebSocket | — | 38 | 3860 | 229 | 81 KB |
| lux-coreRutas, inyección, JSON, validación, seguridad, métricas, caché, eventos, OpenAPI | lux-http | 63 | 5473 | 459 | 147 KB |
| lux-viewPlantillas con herencia, bucles y escapado por defecto | lux-core | 9 | 974 | 88 | 38 KB |
| lux-dataJDBC, pool, transacciones y repositorios | lux-core | 14 | 1233 | 286 | 35 KB |
| lux-adapter-servletDesplegar una aplicación LuxCore dentro de Tomcat 10.1+ | lux-core | 4 | 588 | 35 | 13 KB |
| Los cinco | — | 126 | 11 759 | 1 047 | 310 KB |
Las 36 pruebas restantes hasta 940 son de la aplicación de ejemplo, que ejercita los cinco módulos de punta a punta.
Servidor · lux-http
HTTP/1.1 con un hilo virtual por conexión. Se puede usar suelto, sin
lux-core, si lo único que quieres es atender peticiones.
Server.start(8080, (req, res) -> res.text("Hola"));
Opciones
| Opción | Por defecto | Para qué |
|---|---|---|
| port | 8080 | 0 asigna uno libre |
| host | 0.0.0.0 | Interfaz de escucha |
| maxConnections | 10 000 | Techo de conexiones concurrentes |
| idleTimeoutMillis | 30 000 | Corta conexiones ociosas |
| handlerTimeoutMillis | 30 000 | Libera la conexión si el handler se cuelga |
| shutdownGraceMillis | 10 000 | Espera a las peticiones en vuelo al apagar |
| maxBodyBytes | 10 MB | Tamaño máximo del cuerpo |
| maxHeaderBytes | 32 768 | Tamaño máximo de cabeceras |
| maxHeaderCount | 100 | Número máximo de cabeceras |
| maxKeepAliveRequests | 1 000 | Peticiones por conexión |
| sessionTimeoutMillis | 30 min | Caducidad de sesión por inactividad |
| gzipMinBytes | 1 024 | Umbral de compresión; 0 la desactiva |
| requireHost | true | Exige Host en HTTP/1.1 |
ServerOptions opciones = ServerOptions.builder()
.port(8443)
.tls(Tls.fromKeystore(Path.of("certificado.p12"), "clave".toCharArray()))
.maxBodyBytes(50L << 20)
.build();
StaticFiles.from(Path.of("publico")) // desde disco
StaticFiles.fromClasspath("estaticos", "/estaticos") // desde el JAR
Los estáticos emiten ETag y Last-Modified, responden 304 ante
If-None-Match, y bloquean cualquier intento de salir del directorio raíz.
Vistas · lux-view
Plantillas compiladas y cacheadas, con herencia. El escapado HTML es el comportamiento por defecto: hay que pedir explícitamente el crudo.
| Sintaxis | Qué hace |
|---|---|
| {{ expr }} | Interpola, escapando el HTML |
| {{& expr }} | Interpola sin escapar |
| {% if c %} … {% end %} | Condicional, con {% elseif %} y {% else %} |
| {% for x in lista %} | Bucle, con {% else %} si va vacía |
| {% include "parcial.html" %} | Inserta otra plantilla |
| {% extends "base.html" %} | Hereda de un layout |
| {% block nombre %} | Define o sobrescribe un bloque |
| Comentario, no sale al HTML |
Dentro de un bucle
| Variable | Valor |
|---|---|
| loop.index | Posición desde 0 |
| loop.number | Posición desde 1 |
| loop.first / loop.last | Si es el primero o el último |
| loop.size | Cuántos elementos hay |
{% extends "base.html" %}
{% block titulo %}Tareas · {{ pendientes }} pendientes{% end %}
{% block contenido %}
{% if tareas %}
<ul>
{% for t in tareas %}
<li class="{{ t.etiqueta }}">
{{ loop.number }}. {{ t.titulo }}
{% if !loop.last %}<hr>{% end %}
</li>
{% end %}
</ul>
{% else %}
<p>No hay ninguna tarea.</p>
{% end %}
{% end %}
Las expresiones admiten rutas con punto (autor.nombre), índices de lista
(items.0), .size, literales, y los operadores
== != < > <= >=, and, or,
! y paréntesis. Nada más: es deliberadamente pequeño.
Templates.fromClasspath("plantillas") // dentro del JAR Templates.from(Path.of("plantillas")) // desde disco .suffix(".html") // para escribir view("lista") .reload(true) // recompila al editar, en desarrollo
Datos · lux-data
JDBC directo, sin ORM. Todo pasa por sentencias preparadas; los nombres de tabla y columna se validan carácter a carácter antes de tocar la base de datos.
DataSources.registerDefault(
Pool.to("jdbc:postgresql://localhost/tareas")
.credentials("usuario", "clave")
.maxSize(10)
.build());
Consultas
Db db = Db.open(); Rows filas = db.select("tareas", "hecha = ?", false); Row una = db.selectOne("tareas", "id = ?", 7); long total = db.count("tareas", null); List<Tarea> t = db.query(Tarea.class, "SELECT * FROM tareas WHERE prioridad = ?", "alta"); Page<Row> pag = db.page("tareas", "hecha = ?", 1, 20, false); db.insert("tareas", Row.of("titulo", "Escribir", "prioridad", "alta")); db.update("tareas", Row.of("hecha", true), "id = ?", 7); db.delete("tareas", "id = ?", 7); // parámetros con nombre db.queryNamed("SELECT * FROM tareas WHERE prioridad = :p", Map.of("p", "alta"));
Repositorios
@Table("tareas") public record Tarea(@Id long id, String titulo, @Column("hecha") boolean completada) {} @Service public class Tareas extends Repository<Tarea, Long> { public Tareas() { super(Tarea.class); } public List<Tarea> pendientes() { return findBy("hecha = ?", false); } } tareas.findById(7L) tareas.findAll() tareas.findPage(1, 20) tareas.insert(tarea) tareas.update(tarea) tareas.deleteById(7L) tareas.count() tareas.existsById(7L)
Transacciones
Tx.run(() -> {
cuentas.debitar(origen, monto);
cuentas.acreditar(destino, monto); // si esto lanza, se deshace todo
});
long id = Tx.call(() -> pedidos.insert(pedido));
- Las consultas dentro de una transacción comparten conexión automáticamente.
- Las transacciones anidadas se unen a la externa; no hay puntos de guardado.
- Los nombres de columna se buscan sin distinguir mayúsculas: H2 y Oracle devuelven
TITULO, PostgreSQLtitulo, y los dos funcionan.
Seguridad
.use(Cors.allowing("https://app.pe").credentials(true)) .use(Csrf.enabled().exempt("/api/")) .use(RateLimit.perMinute(300).keyBy(ctx -> ctx.header("X-Cliente"))) .use((ctx, cadena) -> { // el tuyo propio long t0 = System.nanoTime(); try { return cadena.proceed(ctx); } finally { registrar(ctx, System.nanoTime() - t0); } })
| Pieza | Qué cubre |
|---|---|
| Cors | Preflight OPTIONS, credenciales, Vary: Origin. Con credenciales nunca responde *: devuelve el origen concreto. |
| Csrf | Token en sesión, comparación en tiempo constante. Lee _csrf de la cabecera, del formulario o de la consulta. Exención por prefijo o con @CsrfExempt. |
| RateLimit | Ventana deslizante ponderada, cabeceras X-RateLimit-* y Retry-After, clave configurable. |
| Sanitize | html() quita script, estilos, iframes y manejadores de evento; text() deja solo texto; filename() neutraliza rutas. |
| @RequireAuth | 401 si no hay principal. |
| @RequireRole("admin") | 401 sin autenticar, 403 con rol insuficiente. |
Autenticación
.authenticator(ctx -> {
String token = ctx.header("Authorization");
if (token == null) return null; // anónimo
return Principal.of("andre", "admin"); // id + roles
})
El orden del pipeline es autenticar → middleware → autorizar → vincular → invocar → renderizar. Así el middleware ve el principal y a la vez puede envolver los 401 y 403 para registrarlos.
Cabeceras plegadas, Content-Length duplicado, Content-Length
junto a Transfer-Encoding, Host ausente o duplicado,
caracteres de control en las cabeceras de respuesta, y salidas del directorio raíz en
los archivos estáticos.
Configuración
loadConfig() lee application.properties del classpath y del
directorio actual, y encima aplica las variables de entorno con prefijo
LUX_ y las propiedades del sistema con prefijo lux.
server.port=8080 server.maxConnections=5000 server.gzipMinBytes=512 db.url=jdbc:postgresql://localhost/tareas db.usuario=lux db.pool=10
Config config = Config.load(); config.get("db.url") config.getInt("db.pool", 5) config.getBoolean("app.debug", false) config.under("db") // o vincularla a un record record Datos(String url, String usuario, int pool) {} Datos datos = config.bind(Datos.class, "db");
Las claves server.* configuran el servidor sin escribir código:
LUX_SERVER_PORT=9000 en el entorno cambia el puerto.