From b53cdbc0772d655bbd3cc27d647de0ab056e086c Mon Sep 17 00:00:00 2001 From: CodingPhoenixx Date: Thu, 28 May 2026 11:10:42 +0200 Subject: [PATCH] Add initial README.md with project overview, features, setup guide, and API examples. --- README.md | 319 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5be3d0 --- /dev/null +++ b/README.md @@ -0,0 +1,319 @@ +# 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 + +```java +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. + +```java +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`. + +```java +@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: + +```java +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: + +```java +AnnotationScanner.register(router, new UserController()); +AnnotationScanner.register(router, new OrderController()); +``` + +--- + +### Middleware + +Middleware runs before the matched handler on every request. + +```java +router.use((req, res) -> { + res.header("X-Request-Id", UUID.randomUUID().toString()); +}); +``` + +--- + +## Request API + +```java +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 +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 + +```java +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 + +```java +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 + +```java +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:`), otherwise the client IP (`ip:`) | +| Custom lambda | `(req, remoteAddr) -> myKey(req)` | + +--- + +## Full Example + +```java +// 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 21+ +- Netty 4.x +- Jackson (tools.jackson)