Skip to main content

Middleware

Middleware is Ligero's extension mechanism: every cross-cutting concern is a middleware, never a core change (the open/closed principle applied literally).

@FunctionalInterface
public interface Middleware {
void handle(Context ctx, Chain chain) throws Exception;
}

Call chain.proceed() to continue; skip it to short-circuit (you are then responsible for the response).

app.use((ctx, chain) -> {
long start = System.nanoTime();
chain.proceed();
log.info("{} took {} µs", ctx.path(), (System.nanoTime() - start) / 1000);
});

app.use("/admin", adminOnlyMiddleware); // path-scoped

Middleware runs in registration order, wrapping the router dispatch: first-in → … → handler → … → first-out.

Built-in middleware

MiddlewarePurpose
RequestIdMiddlewareX-Request-Id passthrough/generation + W3C traceparent propagation
RequestLoggingMiddleware / .json()access log, plain or structured JSON
SecurityHeadersMiddlewarenosniff, X-Frame-Options, Referrer-Policy, optional HSTS/CSP
CorsMiddlewareCORS with preflight handling
RateLimitMiddlewaretoken bucket, 429 on excess; pluggable RateLimiterStore
BasicAuthMiddlewareHTTP Basic with pluggable validator
StaticFilesMiddlewarestatic assets from disk or classpath, traversal-safe, ETag/304
HealthMiddleware/health endpoint with named readiness checks
MetricsMiddlewareper-route counters/latency via the MetricsCollector SPI

From ligero-auth: JwtAuthMiddleware, CsrfMiddleware, SessionMiddleware. From ligero-openapi: OpenApi.

Example stack

app.use(new RequestIdMiddleware());
app.use(RequestLoggingMiddleware.json());
app.use(SecurityHeadersMiddleware.defaults());
app.use(CorsMiddleware.builder().allowOrigins("https://app.example").build());
app.use(RateLimitMiddleware.of(100, 100));
app.use(HealthMiddleware.defaults());
app.use(StaticFilesMiddleware.classpath("/assets", "web"));
app.use("/api", JwtAuthMiddleware.of(jwt));

Static files in detail

app.use(StaticFilesMiddleware.external("/static", Path.of("public")));
app.use(StaticFilesMiddleware.classpath("/assets", "web").cacheControl("public, max-age=86400"));
  • Path traversal (.., encoded variants, backslashes) is rejected — covered by tests.
  • External files get an ETag; If-None-Match yields 304.
  • Misses fall through to your routes and 404 handling.