Axiom
Documentation: https://matrixjnr.github.io/axiom/
Axiom is an early, pre-release Java 21 framework for HTTP APIs. Applications register
handlers on compiled routes; an HTTP/1.1 listener built on Netty runs each request on
a virtual thread under a deadline and bounded admission, request bodies are bounded
and decoded by a strict JSON codec, and every error is an application/problem+json
response that never leaks internals. The same dispatcher runs in memory through
TestClient, so applications are tested without opening a port. Nothing is published
to Maven Central yet.
Hello, world
import com.jsgalactic.axiom.Axiom;
public class Hello {
public static void main(String[] args) throws Exception {
try (var app = Axiom.create()) {
app.get("/", ctx -> "Hello, world!");
app.listen(8080).termination().toCompletableFuture().join();
}
}
}
listen(8080) binds 127.0.0.1; see HTTP listeners for other addresses.
A JSON API
Records as bodies, a path parameter, a query parameter, a route group with middleware, an error handler and a validated body. It needs only the starter.
import com.jsgalactic.axiom.Axiom;
import com.jsgalactic.axiom.application.Application;
import com.jsgalactic.axiom.context.BodyValidator;
import com.jsgalactic.axiom.context.Middleware;
import com.jsgalactic.axiom.error.NotFoundException;
import com.jsgalactic.axiom.error.Violation;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public final class TasksApi {
public record NewTask(String title) {}
public record Task(String id, String title) {}
static final BodyValidator<NewTask> VALID = task -> task.title() == null || task.title().isBlank()
? List.of(new Violation("title", "required")) : List.of();
static final Middleware NO_SNIFF = (ctx, next) -> next.run().withHeader("X-Content-Type-Options", "nosniff");
static Application create() {
var tasks = new ConcurrentHashMap<String, Task>();
var ids = new AtomicLong();
var app = Axiom.create();
app.error(NoSuchElementException.class, (ctx, e) -> { throw new NotFoundException("task_not_found"); });
app.group("/tasks", api -> {
api.use(NO_SNIFF);
api.post("", ctx -> { // POST /tasks, 422 on a blank title
var task = new Task(Long.toString(ids.incrementAndGet()), ctx.validatedBody(NewTask.class, VALID).title());
tasks.put(task.id(), task);
return ctx.status(201).json(task).withLocation("/tasks/" + task.id());
});
api.get("", ctx -> { // GET /tasks?q=milk
var text = ctx.query("q").orElse("");
return ctx.json(tasks.values().stream().filter(t -> t.title().contains(text)).toList());
});
api.get("/:id", ctx -> ctx.json(Optional.ofNullable(tasks.get(ctx.path("id"))).orElseThrow()));
});
return app;
}
public static void main(String[] args) throws Exception {
create().listen(8080).termination().toCompletableFuture().join();
}
}
Tested without a socket:
try (var client = TestClient.start(TasksApi.create())) {
var created = client.post("/tasks", "application/json", "{\"title\":\"Buy milk\"}");
assertThat(created.status()).isEqualTo(201);
assertThat(client.get("/tasks/42").status()).isEqualTo(404); // problem+json, code task_not_found
}
These three blocks are quoted from examples/readme, which the build compiles and
tests; checkReadmeSnippets fails when the README and the source differ.
examples/rest-api is a complete version with tests.
What works today
- HTTP/1.1 listener on Netty with keep-alive and pipelining; Netty stays behind the public API (HTTP listeners).
- Virtual-thread execution with a per-request deadline (504) and cancellation (execution).
- Bounded admission: aggregate and per-route active limits with bounded waiting queues (503) and observable counters (admission).
- Compiled routing on an immutable segment trie: named parameters, terminal
wildcards, deterministic precedence, ambiguous templates rejected at registration,
HEAD fallback to GET, automatic OPTIONS, 405 with
Allow(routing). - Query parameters decoded as strict UTF-8 with bounds (routing).
- Bounded request bodies (Content-Length, chunked,
Expect: 100-continue) and a strict Jackson JSON codec (bodies). - Streaming responses and server-sent events:
Response.streamandResponse.ssewrite the body from the handler’s virtual thread with chunked encoding, backpressure from slow clients, a byte cap and the request deadline; a disconnect, cap or shutdown aborts the writer and closes the connection, andTestClient.streamreads them deterministically (streaming). - Errors:
application/problem+jsonfor every error, exceptions for common 4xx/5xx statuses, and a never-leak rule (errors). - Middleware, route groups and error handlers, composed once at startup (middleware).
- Validation modules: annotation-free rules and a Jakarta Validation adapter, reported as 422 field violations (validation).
TestClient: in-memory requests through the real dispatcher, admission and deadlines (programming model).- Graceful drain: closing a listener stops admission, lets running handlers finish within a grace period, then interrupts them (HTTP listeners).
- TLS on the HTTP/1.1 listener with the JDK’s TLS: PEM or
SSLContextkey material validated at startup, TLS 1.2 minimum with modern suites, optional mutual TLS, certificate reload without a restart, ALPNhttp/1.1, andRequest.isSecure()(TLS). - Security modules (opt-in): a request-scoped
SecurityIdentity, anAuthenticatorSPI,authenticated()/hasRole/hasPermissionpolicies (401 vs 403 problem responses), a strict JDK-only JWT authenticator, trusted-proxy client addresses, header redaction and secure default headers (security). - Observability (opt-in registry): a dependency-free
MetricsSPI instrumenting requests by route template and status class, latency, admission rejections and queue depth and wait; an in-memory registry with Prometheus text output; liveness and readiness routes that turn unready while draining; and a strictly parsed W3Ctraceparenton the context (observability). - Secure path handling: raw paths with dot or empty segments, backslashes or encoded separators are rejected with 400, never normalized (routing).
Not yet
- HTTP/2
- WebSocket
- Streaming request bodies (request bodies are buffered in memory)
- Sessions, cookie authentication, CSRF, CORS and OAuth flows; JWKS key fetching
- OpenTelemetry, tracing spans and metrics exporters other than Prometheus text
- OpenAPI generation
- Native transports (epoll, io_uring)
- Publication to Maven Central
Known limitations are tracked in #13 and remaining milestones in the roadmap, #18.
Modules
| Module | Artifact ID | Purpose |
|---|---|---|
axiom-starter |
axiom |
The one dependency for applications: core API at compile time; HTTP, server and JSON at run time |
axiom-core |
axiom-core |
Application contracts, request/response values, errors and the bootstrap SPI |
axiom-server |
axiom-server |
Lifecycle, compiled route dispatch, admission and the default runtime |
axiom-http |
axiom-http |
HTTP/1.1 transport on Netty |
axiom-json |
axiom-json |
Strict JSON codec on Jackson, loaded at run time |
axiom-test |
axiom-test |
In-memory TestClient |
axiom-validation |
axiom-validation |
Validator interface and annotation-free rules (opt-in) |
axiom-validation-jakarta |
axiom-validation-jakarta |
Jakarta Validation through Hibernate Validator (opt-in) |
axiom-security |
axiom-security |
Authenticator SPI, role and permission policies, trusted proxies, header redaction, security headers (opt-in) |
axiom-security-jwt |
axiom-security-jwt |
Strict JWT bearer-token authenticator on the JDK only (opt-in) |
axiom-metrics |
axiom-metrics |
Bounded in-memory metrics registry and Prometheus text output (opt-in) |
axiom-bom |
axiom-bom |
Bill of materials aligning all Axiom versions |
integration-tests |
not published | JSON contract tests with the real codec, in memory and over a live listener |
benchmarks/http |
not published | JMH microbenchmarks; no performance claims (benchmarks) |
examples/hello, examples/rest-api |
not published | Runnable examples |
All artifacts use the group com.jsgalactic.axiom. Netty and Jackson never appear in
an Axiom API; core depends only on the JDK (build decisions).
Install
Not yet published to Maven Central. These are the intended coordinates; they will not resolve until a first release. Publishing them is tracked in #19 and releasing.
Gradle (Kotlin DSL):
dependencies {
implementation(platform("com.jsgalactic.axiom:axiom-bom:VERSION"))
implementation("com.jsgalactic.axiom:axiom")
testImplementation("com.jsgalactic.axiom:axiom-test")
}
Gradle (Groovy DSL):
dependencies {
implementation platform('com.jsgalactic.axiom:axiom-bom:VERSION')
implementation 'com.jsgalactic.axiom:axiom'
testImplementation 'com.jsgalactic.axiom:axiom-test'
}
Maven:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.jsgalactic.axiom</groupId>
<artifactId>axiom-bom</artifactId>
<version>VERSION</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.jsgalactic.axiom</groupId>
<artifactId>axiom</artifactId>
</dependency>
<dependency>
<groupId>com.jsgalactic.axiom</groupId>
<artifactId>axiom-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Validation is opt-in: add com.jsgalactic.axiom:axiom-validation, or
com.jsgalactic.axiom:axiom-validation-jakarta for Jakarta annotations (versions from
the BOM). See validation.
Security
Security is opt-in too: add com.jsgalactic.axiom:axiom-security, or
com.jsgalactic.axiom:axiom-security-jwt for bearer JWTs (it brings axiom-security).
Neither adds a third-party dependency.
var jwt = JwtAuthenticator.builder()
.publicKey(JwsAlgorithm.RS256, issuerPublicKey) // alg allow-list = registered keys
.issuer("https://login.example.com")
.audience("notes-api")
.build();
var security = Security.of(jwt);
app.use(SecurityHeaders.defaults()); // nosniff, frame and referrer policies, CSP
app.get("/me", ctx -> ctx.identity().orElseThrow().principal(), security.authenticated());
app.group("/admin", admin -> admin.use(security.hasRole("admin"))); // 401 anonymous, 403 otherwise
Missing credentials answer 401 with WWW-Authenticate; malformed, expired or forged
tokens 401 invalid_token (never echoing the token); a missing role or permission 403.
Forwarded client addresses are believed only from configured proxies
(TrustedProxies.of("10.0.0.0/8").resolve(ctx.request())). See security.
Observability
Metrics, health checks and trace context need no extra dependency; axiom-metrics adds an
in-memory registry and a Prometheus renderer.
var registry = MetricsRegistry.create();
var app = Axiom.create().metrics(registry); // requests, latency, admission, queue depth
var health = Health.builder(app).readiness("database", () -> pool.isValid(1)).build();
health.register(app, security.hasRole("ops")); // GET /health/startup, /health/live, /health/ready
app.get("/metrics", PrometheusText.handler(registry), security.hasRole("ops"));
// shutdown: app.closeOnJvmShutdown(Duration.ofSeconds(10)) drains readiness, waits, then closes
Tags are route templates and status classes, never raw paths or user input. Do not expose
/metrics or the health routes publicly without authentication. See
observability.
Build and test
JDK 21 and the committed Gradle wrapper (on Windows, gradlew.bat):
| Command | Runs |
|---|---|
./gradlew check |
Compilation, unit and integration tests, module boundary and API checks |
./gradlew unitTest |
Every module’s test task (no sockets) |
./gradlew integrationTest |
Every module’s integrationTest task (live listeners) |
./gradlew coverageReport |
Per-module and aggregated JaCoCo reports (build/reports/jacoco/coverageReport) |
./gradlew compatibilityTest |
Gradle Kotlin, Gradle Groovy and Maven consumer builds against locally published artifacts |
./gradlew :examples:hello:run |
Hello World on http://127.0.0.1:8080/ |
./gradlew :examples:rest-api:run |
The notes JSON API on the same port |
Details are in build decisions.
Documentation
- Programming model, routing, request bodies and JSON, streaming and server-sent events, errors, middleware, validation, security, observability
- HTTP listeners, TLS, execution and deadlines, admission, benchmarks
- Build decisions, releasing, changelog
Contributing and security
See CONTRIBUTING and the code of conduct. Report vulnerabilities privately as described in SECURITY. Issues and ideas go to the issue tracker.
Support Axiom
If Axiom is useful to you, buy me a coffee via PayPal
to support its development. Choose any amount; payments go to jonnysimiyu@gmail.com.
License
Licensed under the Apache License 2.0.