ullun-api 0.4.12

Express.js-inspired web framework for Rust - simple, fast, batteries-included
Documentation
# ullun-api Architecture

## Principles

1. **Simple surface** - an Express-style builder (`App::new().get(..).middleware(..).run(..)`); everything else is an opt-in module.
2. **Thin core on hyper/tokio** - the core adds routing, middleware and conversions, not a runtime of its own.
3. **Safe defaults** - limits, timeouts and header hygiene are on or one call away; unsafe code is not used.
4. **Few dependencies** - the core needs `hyper`, `tokio`, `matchit`, `serde`; heavier things (`rustls`, `hmac`, `flate2`, WebSocket) sit behind cargo features.
5. **Testable without sockets** - the whole request pipeline can run in-process (`testing::TestClient`).

## Layers

```
                 ┌──────────────────────────────────────────────────────────┐
 application     │ handlers, App builder, route groups                      │
                 └──────────────────────────────────────────────────────────┘
                 ┌──────────────────────────────────────────────────────────┐
 middleware      │ cors, security_headers, session, csrf, jwt, rate_limit,  │
 (opt-in)        │ limits, idempotency, cache, compression, conditional,    │
                 │ observability, trace, problem, secure                    │
                 └──────────────────────────────────────────────────────────┘
                 ┌──────────────────────────────────────────────────────────┐
 core            │ app, router, handler, middleware, request, response,     │
                 │ error, cookies, upload, stream, shutdown                 │
                 └──────────────────────────────────────────────────────────┘
                 ┌──────────────────────────────────────────────────────────┐
 transport       │ hyper (HTTP/1.1, HTTP/2), tokio, tls (rustls)            │
                 └──────────────────────────────────────────────────────────┘
```

Modules above the core never reach into `app.rs`; they talk to the framework only through `Middleware`, `Handler`, `Request` and `Response`. That keeps them replaceable and individually testable.

## Module map

| Area | Modules |
|------|---------|
| Core | `app` (builder, accept loop, connection driving), `router` (matchit per method), `handler`, `middleware` (`Middleware`, `Next`, `Layered`), `request`, `response`, `error`, `cookies`, `shutdown` |
| Request bodies | `upload` (streaming), `multipart`, `validate` (field errors → 422) |
| Responses | `sse`, `stream`, `files` (ranges, conditional), `static_files` (precompressed, SPA), `template` |
| Security | `cors`, `security_headers`, `session`, `csrf`, `jwt`, `auth`, `signed_cookies`, `proxy`, `security` (tokens, SSRF checks), `secure` (HTTPS policy), `tls`, `rate_limit` |
| Reliability | `limits` (timeouts, concurrency, connection caps), `health`, `idempotency`, `cache`, `jobs`, `hub`, `compression`, `conditional` |
| Observability | `observability` (request id, access log, metrics), `trace` (W3C trace context) |
| API contract | `openapi`, `problem` (RFC 9457), `negotiate` |
| Outbound / tests | `client` (HTTP client), `testing` (in-process `TestClient`) |

Optional dependencies are behind features: `jwt`/`signing` (hmac, sha2, base64), `compression` (flate2), `websocket`, `tls` and `client-tls` (rustls, webpki-roots).

## Request flow

```
TCP accept (app.rs)
  ├─ ConnLimits.admit()        total / per-IP caps → 503 and close
  ├─ TLS handshake (tls feature, own task, timeout) → ALPN: h2 | http/1.1
  └─ serve_connection
       ├─ http1::Builder  (header_read_timeout, upgrades for WebSocket)
       └─ http2::Builder  (stream cap, keep-alive pings)
            │
            ▼
       handle_request(hyper::Request<Incoming>)
         ├─ body: buffered up to max_body_size / body_limit, or left unread for stream_body routes
         ├─ :authority → Host header (HTTP/2)
         └─ run_pipeline(Request)                       ← shared with TestClient
              ├─ Router::match_route_with_pattern → Found(handler) | Miss(allow)
              ├─ middleware chain, in registration order
              │    (global middleware first, then group middleware, then the handler)
              ├─ Err(e) → Response::from_error
              └─ HEAD → headers only
            │
            ▼
       Response::into_hyper → written by hyper
```

Everything between `handle_request` and `into_hyper` is plain data in, plain data out, which is why `TestClient` can call `run_pipeline` directly.

### Middleware order

Middleware wraps from the outside in: the first registered runs first on the way in and last on the way out. A sensible order:

1. `TraceLayer`, `RequestIdLayer`, `AccessLog`, `Metrics` - see everything, including rejections
2. `SecurityHeaders`, `Cors` - decorate every response, including errors and preflights
3. `ProblemDetails` - convert error bodies produced further in
4. `Timeout`, `ConcurrencyLimit`, `RateLimiter` - shed load before doing work
5. `SessionLayer`, `Csrf`, `JwtAuth` - identity
6. `Idempotency`, `ResponseCache`, `Compression`, `ETags` - per-route behaviour (use groups to scope them)

`examples/production` wires this order end to end and is tested as a whole.

Middleware also runs for unmatched routes (404/405/OPTIONS), so CORS preflights and request ids work without a route.

## Extension points

| Trait | Purpose | Default |
|-------|---------|---------|
| `Handler` | anything that answers a request; implemented for async closures and `async fn(Request)` | - |
| `Middleware` | `(Request, Next) → Result<Response>`; implemented for async closures | - |
| `SessionStore` | where session data lives | in-memory |
| `IdempotencyStore` | where idempotency keys and replays live | in-memory |
| `TemplateEngine` | rendering | built-in engine |

In-memory defaults are bounded (entry caps) and process-local; implement the trait for Redis or SQL to share state between instances.

## Error handling

Handlers return `Result<Response, Error>`. `Error` maps to a status code (`BadRequest` 400 … `Validation` 422, `Custom(status, msg)`), and `Response::from_error` renders `{"error", "status"}` JSON (plus `fields` for validation). `ProblemDetails` rewrites that into RFC 9457. `ClientError` (outbound client) converts into `Error` with 502/504, so `?` works across service calls. A panic in a handler is caught around the handler call (`App::catch_panics`, on by default), logged, and answered with a generic `500` that middleware still sees; with it off the panic ends only that connection. `App::fallback` replaces the plain 404 for unknown paths.

## Concurrency model

- One tokio task per connection; HTTP/2 multiplexes requests on it.
- Shared state is `Arc`; handlers are `Send + Sync` and take `Request` by value.
- Background work (`App::background`, `JobQueue`) gets a `Shutdown` signal; graceful shutdown stops accepting, drains requests and tasks for `shutdown_grace`, after an optional `shutdown_delay` during which readiness reports "draining".
- Blocking or CPU-heavy work belongs in `spawn_blocking`; the framework never blocks the runtime itself.

## Testing strategy

| Level | Tool |
|-------|------|
| Units | `#[cfg(test)]` in each module (parsers, stores, math) |
| Pipeline | `testing::TestClient` - routing, middleware, cookies, no sockets |
| Wire | `tests/common` spawns an `App` on port 0 and talks real HTTP (limits, timeouts, TLS, HTTP/2, WebSockets, streaming) |
| Examples | every `examples/<name>` has `tests/example_<name>.rs` that includes its `app.rs` |
| Performance | `benches/` (criterion) and `bench/` (cross-framework load test, see PERFORMANCE.md) |

## Design decisions

- **matchit** for routing: radix tree, parameters, catch-alls, maintained, used widely.
- **Runtime state over proc macros**: routes are registered with plain method calls; compile times stay low and nothing is hidden.
- **`Request` owns its body**: simple handlers by default (`req.json()`), with an explicit opt-in for streaming where memory matters.
- **Middleware as a trait object chain** (`Arc<Vec<Arc<dyn Middleware>>>` plus an index): one allocation-free `Arc` clone and one boxed future per layer, measured at roughly 0.2 µs.
- **Own small HTTP client** instead of a re-export: it shares the SSRF guard, tracing and error types with the server side.
- **Deliberately not included**: an ORM, a config system, background-job persistence beyond the in-process queue, and an OpenTelemetry exporter.