A lightweight Java library for creating HTTP servers based on OpenAPI specifications.
This library provides a simple way to create an HTTP server that implements OpenAPI specifications.
It is designed to be simple to use while providing the essential features needed for creating efficient HTTP servers in Java.
- Java SDK 25 or later
- A serialization library, e.g. Gson or Jackson
- OpenAPI specification file in JSON format (
openapi.json)
- Create an OpenAPI specification file named
openapi.jsonin your project resources. - Define your HTTP handlers by implementing the
HttpHandlerinterface:
public class GetDataHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try (exchange) {
byte[] bytes = """
{
"id": "some-id"
}""".getBytes();
var responseHeaders = exchange.getResponseHeaders();
responseHeaders.add("content-type", "application/json");
exchange.sendResponseHeaders(HTTP_OK, bytes.length);
try (var os = exchange.getResponseBody()) {
os.write(bytes);
}
}
}
}
public class PostDataHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try (exchange) {
// Access the raw request body bytes.
byte[] body = Request.bytes(exchange);
// Or get the already-parsed object (Map or List) produced by your JsonMapper.
Object parsed = Request.parsed(exchange);
exchange.sendResponseHeaders(HTTP_OK, -1);
}
}
}- Initialize the server (using Gson in this example):
public class YourServerLauncher {
public static void main(String[] args) throws Exception {
Gson gson = new Gson();
// Parse spec to a generic Map (works for JSON; for YAML use SnakeYAML).
String text = Files.readString(Path.of("openapi.json"));
Map<String, Object> raw = (Map<String, Object>) gson.fromJson(text, Map.class);
Spec spec = Spec.from(raw);
// Body parser. Returns a Map for objects, List for arrays.
JsonMapper mapper = body -> gson.fromJson(new String(body), Object.class);
// Handlers by operationId.
Map<String, HttpHandler> handlers = new HashMap<>();
handlers.put("get-data", new GetDataHandler());
handlers.put("post-data", new PostDataHandler());
new OpenApiServer(spec, mapper, handlers, Handlers.defaultExceptionHandler());
}
}For YAML, replace the JSON parsing line with SnakeYAML:
Map<String, Object> raw = new Yaml().load(Files.newInputStream(Path.of("openapi.yaml")));The rest is identical.
- OpenAPI specification support
- Automatic request body parsing for JSON arrays and objects
- Custom HTTP handler support
- Built on Java's native
HttpServerwith Thread-Per-Request behaviour using Virtual Threads. - Custom integration for JSON serialization/deserialization
Handlers are registered using string keys that correspond to your OpenAPI operation IDs.
The library uses a flexible JSON mapping system that automatically detects and parses (using a mapper of choice):
- JSON arrays (
[...]) - JSON objects (
{...})
To test the server in isolation, you can start an example server (src/test/java/com/retailsvc/http/start/ServerLauncher.java).
Schemas are located under test resources folder.
- Example requests can be found under
acceptance/k6that can be a base for exploring the functionality. - The logger in the configuration needs to be enabled to get some insight into the code.
The library wraps the JDK's bundled com.sun.net.httpserver.HttpServer and uses a virtual-thread-per-request executor. On a developer laptop (Apple Silicon, single instance, default JVM flags) it sustains roughly:
- ~32k requests/second for small JSON GETs and POSTs (~300 byte bodies), measured via
k6at 30 sustained VUs over 45 seconds (1.4M requests, 100% of checks passing, 0% HTTP failures).
A few things to know:
- Single-process model. No horizontal scaling primitives are bundled; run multiple instances behind a load balancer for production scale.
- JDK HttpServer is the throughput ceiling. It's documented as a low-throughput / dev-test server. If you need to go materially above the rates above, deploy the same filter/validator/router stack on Jetty, Helidon Níma, or Netty — the spec and validation code is server-agnostic.
- Per-request state uses
ScopedValue(Java 25, JEP 506), notHttpExchange.setAttribute. This matters if a handler offloads work to an executor that's not aStructuredTaskScope-managed child thread: theScopedValueis not visible there, so the handler must capture the values it needs (e.g.byte[] body = Request.bytes();) before submitting. HttpExchange.sendResponseHeaders(rCode, length)gotcha. When a handler has no response body, pass-1(Content-Length: 0, no body); passing0produces a chunked response with zero chunks, which is technically non-conformant.