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)