LuxCore

Módulos

Cuatro piezas que se toman por separado. Si solo necesitas atender HTTP, lux-http basta y son 64 KB.

01

Los cinco módulos

Cada módulo se toma por separado. Si solo necesitas atender HTTP, lux-http basta y son 64 KB.

MóduloNecesitaClasesLíneasPruebasTamaño
lux-httpServidor HTTP/1.1, TLS recargable, sesiones enchufables, multipart, gzip, estáticos con rangos, WebSocket 38386022981 KB
lux-coreRutas, inyección, JSON, validación, seguridad, métricas, caché, eventos, OpenAPI lux-http635473459147 KB
lux-viewPlantillas con herencia, bucles y escapado por defecto lux-core99748838 KB
lux-dataJDBC, pool, transacciones y repositorios lux-core14123328635 KB
lux-adapter-servletDesplegar una aplicación LuxCore dentro de Tomcat 10.1+ lux-core45883513 KB
Los cinco12611 7591 047310 KB

Las 36 pruebas restantes hasta 940 son de la aplicación de ejemplo, que ejercita los cinco módulos de punta a punta.

02

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.

Sin frameworkJava
Server.start(8080, (req, res) -> res.text("Hola"));

Opciones

OpciónPor defectoPara qué
port80800 asigna uno libre
host0.0.0.0Interfaz de escucha
maxConnections10 000Techo de conexiones concurrentes
idleTimeoutMillis30 000Corta conexiones ociosas
handlerTimeoutMillis30 000Libera la conexión si el handler se cuelga
shutdownGraceMillis10 000Espera a las peticiones en vuelo al apagar
maxBodyBytes10 MBTamaño máximo del cuerpo
maxHeaderBytes32 768Tamaño máximo de cabeceras
maxHeaderCount100Número máximo de cabeceras
maxKeepAliveRequests1 000Peticiones por conexión
sessionTimeoutMillis30 minCaducidad de sesión por inactividad
gzipMinBytes1 024Umbral de compresión; 0 la desactiva
requireHosttrueExige Host en HTTP/1.1
TLS y estáticosJava
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.

03

Vistas · lux-view

Plantillas compiladas y cacheadas, con herencia. El escapado HTML es el comportamiento por defecto: hay que pedir explícitamente el crudo.

SintaxisQué 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

VariableValor
loop.indexPosición desde 0
loop.numberPosición desde 1
loop.first / loop.lastSi es el primero o el último
loop.sizeCuántos elementos hay
lista.htmlPlantilla
{% 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.

Configurar el motorJava
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
04

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.

ConectarJava
DataSources.registerDefault(
    Pool.to("jdbc:postgresql://localhost/tareas")
        .credentials("usuario", "clave")
        .maxSize(10)
        .build());

Consultas

DbJava
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

RepositoryJava
@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

TxJava
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, PostgreSQL titulo, y los dos funcionan.
05

Seguridad

MiddlewareJava
.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); }
})
PiezaQué cubre
CorsPreflight OPTIONS, credenciales, Vary: Origin. Con credenciales nunca responde *: devuelve el origen concreto.
CsrfToken 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.
RateLimitVentana deslizante ponderada, cabeceras X-RateLimit-* y Retry-After, clave configurable.
Sanitizehtml() quita script, estilos, iframes y manejadores de evento; text() deja solo texto; filename() neutraliza rutas.
@RequireAuth401 si no hay principal.
@RequireRole("admin")401 sin autenticar, 403 con rol insuficiente.

Autenticación

AuthenticatorJava
.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.

Lo que el servidor rechaza de oficio

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.

06

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.

application.propertiesProperties
server.port=8080
server.maxConnections=5000
server.gzipMinBytes=512

db.url=jdbc:postgresql://localhost/tareas
db.usuario=lux
db.pool=10
LeerlaJava
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.