Add initial README.md with project overview, features, setup guide, and API examples.

This commit is contained in:
CodingPhoenixx
2026-05-28 11:10:42 +02:00
parent 001b70fe09
commit b53cdbc077
+319
View File
@@ -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<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
```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:<token>`), otherwise the client IP (`ip:<addr>`) |
| 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)