Files
Nextus-Web/README.md
T
2026-05-28 11:22:10 +02:00

9.0 KiB

NexusWeb

A lightweight, high-performance HTTP server library built on top of Netty. NexusWeb provides a fluent API for routing, middleware, CORS handling, and rate limiting — all running on Java virtual threads for maximum concurrency.

Features

  • Netty-based — uses epoll/kqueue/NIO automatically based on the platform
  • Virtual thread dispatch — each request is handled on a Java 21 virtual thread
  • Trie-based router — supports static paths, path parameters ({id}), and wildcards (*)
  • Annotation-based controllers — define routes declaratively with @Controller, @GET, @POST, etc.
  • Middleware chain — attach cross-cutting logic to all routes
  • CORS support — configurable origins, methods, headers, credentials, and preflight caching
  • Rate limiting — four algorithm implementations with per-IP, per-token, or custom key strategies
  • JSON I/O — built-in Jackson integration for request parsing and response serialization

Quick Start

Router router = new Router();

router.get("/hello", (req, res) -> {
    res.status(200).json("{\"message\":\"Hello, World!\"}");
});

HttpServer.builder(8080, router).start();

Routing

Routes are registered with GET, POST, PUT, DELETE, or the generic register method.

Router router = new Router();

// Static path
router.get("/users", (req, res) -> { ... });

// Path parameter
router.get("/users/{id}", (req, res) -> {
    String id = req.pathParam("id");
    res.json("{\"id\":\"" + id + "\"}");
});

// Wildcard
router.get("/files/*", (req, res) -> { ... });

// POST with JSON body
router.post("/users", (req, res) -> {
    MyDto dto = req.jsonAs(MyDto.class);
    res.status(201).json(dto);
});

Router resolution

Outcome HTTP Status
Path and method match Handler is called
Path exists, method not registered 405 Method Not Allowed + Allow header
Path not found 404 Not Found

Annotation-based Controllers

Instead of registering lambdas on the Router directly, you can declare routes as methods on a class and register the whole object at once via AnnotationScanner.

Annotations

Annotation Target Description
@Controller("prefix") class Optional path prefix applied to all methods in the class
@GET("/path") method Maps a GET route
@POST("/path") method Maps a POST route
@PUT("/path") method Maps a PUT route
@DELETE("/path") method Maps a DELETE route
@PATCH("/path") method Maps a PATCH route
@Route(method="…", path="…") method Generic — any standard HTTP method by name
@CUSTOM(method="…", value="…") method Custom/non-standard HTTP methods

Every annotated method must have the exact signature (Request req, Response res) and return void.

@Controller("/users")
public class UserController {

    @GET("")
    public void list(Request req, Response res) {
        res.json("[{\"id\":1}]");
    }

    @GET("/{id}")
    public void get(Request req, Response res) {
        res.json("{\"id\":\"" + req.pathParam("id") + "\"}");
    }

    @POST("")
    public void create(Request req, Response res) {
        UserDto dto = req.jsonAs(UserDto.class);
        // ... persist ...
        res.status(201).json(dto);
    }

    @PUT("/{id}")
    public void update(Request req, Response res) {
        // ...
    }

    @DELETE("/{id}")
    public void delete(Request req, Response res) {
        res.status(204);
    }

    @PATCH("/{id}")
    public void patch(Request req, Response res) {
        // ...
    }
}

Register the controller with:

Router router = new Router();
AnnotationScanner.register(router, new UserController());

HttpServer.builder(8080, router).start();

AnnotationScanner.register prints each registered route to stdout:

Registered: GET    /users                         -> UserController.list
Registered: GET    /users/{id}                    -> UserController.get
Registered: POST   /users                         -> UserController.create

Multiple controllers can be registered on the same router:

AnnotationScanner.register(router, new UserController());
AnnotationScanner.register(router, new OrderController());

Middleware

Middleware runs before the matched handler on every request.

router.use((req, res) -> {
    res.header("X-Request-Id", UUID.randomUUID().toString());
});

Request API

req.pathParam("id")           // path parameter, e.g. from /users/{id}
req.queryParam("search")      // first value of ?search=
req.queryParams("tag")        // all values of ?tag= as List<String>
req.header("Authorization")   // raw header value
req.body()                    // raw body as UTF-8 String
req.json()                    // body parsed as Jackson JsonNode
req.jsonAs(MyDto.class)       // body deserialized into a POJO
req.method()                  // HttpMethod
req.path()                    // decoded path without query string

json() and jsonAs() throw BadRequestException (→ 400) on malformed JSON.


Response API

res.status(201)
   .header("X-Custom", "value")
   .json("{\"ok\":true}");    // sets Content-Type: application/json

res.text("plain text");       // sets Content-Type: text/plain
res.json(someObject);         // serializes POJO via Jackson

CORS

CorsConfig config = CorsConfig.builder()
        .allowedOrigins("https://app.example.com")
        .allowedMethods(HttpMethod.GET, HttpMethod.POST)
        .allowedHeaders("Content-Type", "Authorization")
        .allowCredentials(true)
        .maxAgeSeconds(3600)
        .build();

CorsHandler cors = new CorsHandler(config);

HttpServer.builder(8080, router)
        .withCorsHandler(cors)
        .start();

Use CorsConfig.permissive() for a development preset that allows any origin with common methods and headers (incompatible with allowCredentials).

Preflight requests (OPTIONS + Access-Control-Request-Method) are handled automatically and short-circuit before the router.


Rate Limiting

Algorithms

Class Description
TokenBucketLimiter Smooth bursts up to a configurable capacity, refills at a steady rate
FixedWindowLimiter Hard limit per fixed time window
SlidingWindowLimiter Weighted sliding window — reduces boundary spikes vs. fixed window
LeakyBucketLimiter Constant outflow rate; excess requests are rejected immediately

Configuration

RateLimitConfig config = RateLimitConfig.builder()
        // Global rule for all routes — keyed by client IP
        .global(new TokenBucketLimiter(100, 200), KeyResolver.clientIp())
        // Stricter rule for a specific path
        .forPath("/login", new FixedWindowLimiter(5, 60_000), KeyResolver.clientIp())
        // Rule for an entire path prefix
        .forPrefix("/api/", new SlidingWindowLimiter(1000, 1000), KeyResolver.userOrIp())
        .build();

RateLimitGate gate = new RateLimitGate(config);

HttpServer.builder(8080, router)
        .withRateLimitGate(gate)
        .start();

When the limit is exceeded the server responds with 429 Too Many Requests and a Retry-After header.

Response headers

Every response automatically includes:

Header Description
X-RateLimit-Limit Configured limit for the matched rule
X-RateLimit-Remaining Remaining requests in the current window
Retry-After Seconds until the client may retry (only on 429)

Key resolvers

Factory Behaviour
KeyResolver.clientIp() Uses X-Forwarded-For if present, otherwise the remote IP
KeyResolver.userOrIp() Uses the Bearer token if present (u:<token>), otherwise the client IP (ip:<addr>)
Custom lambda (req, remoteAddr) -> myKey(req)

Full Example

// Controller class
@Controller("/users")
public class UserController {

    @GET("")
    public void list(Request req, Response res) {
        res.json("[{\"id\":1}]");
    }

    @GET("/{id}")
    public void get(Request req, Response res) {
        res.json("{\"id\":\"" + req.pathParam("id") + "\"}");
    }

    @POST("")
    public void create(Request req, Response res) {
        UserDto dto = req.jsonAs(UserDto.class);
        res.status(201).json(dto);
    }
}

// Server setup
Router router = new Router();

// Middleware — add request ID to every response
router.use((req, res) -> res.header("X-Request-Id", UUID.randomUUID().toString()));

AnnotationScanner.register(router, new UserController());

CorsHandler cors = new CorsHandler(CorsConfig.permissive());

RateLimitConfig rlConfig = RateLimitConfig.builder()
        .global(new TokenBucketLimiter(200, 400), KeyResolver.clientIp())
        .build();
RateLimitGate gate = new RateLimitGate(rlConfig);

HttpServer.builder(8080, router)
        .withCorsHandler(cors)
        .withRateLimitGate(gate)
        .start();

Requirements

  • Java 26+
  • Netty 4.x
  • Jackson (tools.jackson)