LuxCore

Guía

Todo lo que hace falta para escribir una aplicación: rutas, parámetros, respuestas, inyección, validación y errores.

01

Rutas

@Route sobre la clase fija la base; los verbos sobre los métodos añaden el resto. Una regla, sin excepciones: un valor vacío mapea a la ruta base.

AnotaciónRuta resultanteNota
@Route("/api/tareas")/api/tareasBase de la clase
@Get/api/tareasValor vacío = la base
@Get("/{id}")/api/tareas/{id}Variable de ruta
@Get("/{id}/notas/{n}")…/{id}/notas/{n}Varias variables
@Get("/archivos/*")/api/tareas/archivos/*Comodín, solo al final
@Post @Put @Patch @DeleteMismo criterio
  • Lo literal gana a lo variable. /tareas/nueva se resuelve antes que /tareas/{id}, sin importar el orden en que se declaren.
  • Sin @Route, la base sale del nombre de la clase: ReporteController/reporte.
  • Las rutas duplicadas fallan al arrancar, no en la primera petición.
  • 405 con Allow. Si la ruta existe pero el verbo no, la respuesta enumera los verbos válidos.
  • HEAD se resuelve contra GET automáticamente, sin cuerpo.

Rutas sueltas, sin controlador

LambdasJava
.routes(r -> r
    .get("/salud", ctx -> "ok")
    .get("/version", ctx -> Map.of("version", "0.2.0"))
    .post("/eco", ctx -> ctx.bodyText()))
02

Parámetros

Los argumentos del método se resuelven por anotación o por tipo. La conversión a int, long, boolean, enumeraciones o fechas es automática; si el valor no convierte, la respuesta es un 400.

AnotaciónDe dónde saleEjemplo
@Path("id")Variable de la ruta@Path("id") long id
@Query("q")Cadena de consulta@Query(value="p", orElse="1") int p
@Form("titulo")Formulario HTML@Form("titulo") String t
@BodyCuerpo JSON@Body Tarea tarea
@Body @ValidJSON + validaciónresponde 422 si falla
@Header("X-Traza")Cabecera@Header("X-Traza") String t
@CookieValue("sesion")Cookie@CookieValue("s") String s

Por tipo, sin anotación

TipoQué recibe
ContextTodo el contexto de la petición
Request / ResponseLos objetos crudos de lux-http
SessionLa sesión, creándola si no existe
PrincipalQuien haya autenticado el Authenticator
PartUn archivo del multipart, por nombre del parámetro

El contexto, si lo prefieres a mano

ContextJava
ctx.pathVariable("id")      ctx.query("q", "por-defecto")   ctx.queryAll("tag")
ctx.form("titulo")         ctx.header("Accept")             ctx.cookie("sesion")
ctx.body(Tarea.class)      ctx.bodyText()                  ctx.bodyBytes()
ctx.part("archivo")        ctx.field("nota")               ctx.parts()
ctx.session()              ctx.principal()                 ctx.authenticated()
ctx.attribute("clave", valor)                                 ctx.route()
03

Respuestas

Lo que devuelve una acción decide el formato, sin anotaciones:

DevuelvesSaleEstado
Stringtext/plain200
nullsin cuerpo204
cualquier objetoapplication/json200
Resultlo que declareel que fijes
ResultJava
Result.text("hola")                     Result.html("<h1>hola</h1>")
Result.json(objeto)                    Result.raw("{\"ya\":\"serializado\"}")
Result.view("lista.html", modelo)    Result.redirect("/")
Result.created(objeto)                 Result.noContent()
Result.status(404, "no existe")

// encadenable
return Result.json(tarea)
             .status(201)
             .header("Location", "/api/tareas/" + tarea.id());
04

Inyección

Marca un servicio con @Service y se construye la primera vez que alguien lo pide. Los controladores lo reciben con @Inject, por campo o por constructor.

ServiciosJava
@Service
public class Catalogo {
    @Inject Repositorio repositorio;      // por campo
}

@Service
public class Facturas {
    private final Catalogo catalogo;
    @Inject Facturas(Catalogo catalogo) {   // por constructor
        this.catalogo = catalogo;
    }
}

// o a mano, útil para sustituir en pruebas
Lux.app().service(Reloj.class, () -> Instant.now())
  • Los servicios son singleton: una instancia por aplicación.
  • Se registran también por sus interfaces, así que puedes pedir el contrato.
  • Las dependencias circulares se detectan y el error nombra la cadena completa.
05

Validación

Las anotaciones van sobre los componentes de un record o los campos de una clase. @Valid junto a @Body valida antes de entrar al método.

AnotaciónCompruebaMensaje por defecto
@RequiredNo nulo, no vacío, no en blancoes obligatorio
@Length(min, max)Longitud del textodebe tener entre N y M caracteres
@Range(min, max)Rango numéricodebe estar entre N y M
@EmailFormato de correono es un correo válido
@Match("regex")Expresión regularno tiene el formato esperado
@OneOf({"a","b"})Valor de una listadebe ser uno de: a, b
@Satisfies(Regla.class)Tu propia reglael de la regla
Tarea.javaJava
public record Tarea(
        @Id long id,
        @Required @Length(min = 3, max = 120) String titulo,
        @OneOf({"baja", "media", "alta"}) String prioridad) {}

// una regla propia
public class Ruc implements Rule<String> {
    public boolean test(String v) { return v.length() == 11 && v.startsWith("20"); }
    public String message()    { return "no es un RUC válido"; }
}

Cuando algo falla, la respuesta es un 422 con el mapa de campos, sin escribirlo:

422 Unprocessable ContentJSON
{
  "error": "Unprocessable Content",
  "message": "hay 2 campo(s) inválido(s)",
  "path": "/api/tareas",
  "fields": {
    "titulo": "debe tener entre 3 y 120 caracteres",
    "prioridad": "debe ser uno de: baja, media, alta"
  }
}

Fuera de una ruta, la misma comprobación a mano:

ValidationJava
Validation.check(tarea);                // lanza ValidationException (422)
Validation.valid(tarea);                // true / false
Map<String,String> p = Validation.problems(tarea);
06

Errores

Lanza HttpException con el estado que quieras, o registra un manejador con @OnError para una excepción de tu dominio.

Manejo de erroresJava
@Get("/{id}")
public Object ver(@Path("id") long id) {
    Tarea t = tareas.findById(id);
    if (t == null) throw new HttpException(404, "no existe la tarea " + id);
    return t;
}

@OnError(SaldoInsuficiente.class)
public Object saldo(SaldoInsuficiente fallo) {
    return Result.json(Map.of("motivo", fallo.getMessage())).status(422);
}
Las excepciones no controladas nunca filtran detalles

Cualquier excepción que no sea HttpException devuelve un 500 con el mensaje genérico «error interno». La traza va al ErrorReporter, no a la respuesta.