This page boots a Chappe HTTP server in under 30 lines of Java, then walks through a typed handler, static file serving, and a minimal wrk load test.

Prerequisites

  • Java 25 — Temurin recommended.

  • Maven 3.9.16 — pinned via .sdkmanrc at the root of the chappe repo.

  • sdk env from the repo root loads the right JDK and Maven. If mvn fails with modelVersion 4.1.0 not supported, switch back to Maven 4: sdk use maven 3.9.16.

  • Optional: wrk for the final load test (brew install wrk or equivalent).

Maven coordinates

<dependency>
    <groupId>io.vidocq.chappe</groupId>
    <artifactId>chappe-api</artifactId>
    <version>0.1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>io.vidocq.chappe</groupId>
    <artifactId>chappe-core</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.vidocq.chappe</groupId>
    <artifactId>chappe-http</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <scope>runtime</scope>
</dependency>

chappe-api is the public API, compiled at compile scope. chappe-core (server engine) and chappe-http (HTTP/1.1 and HTTP/2 protocols) sit at runtime scope, discovered via ServiceLoader (ServerProvider).

Hello world

import io.vidocq.chappe.api.*;

void main() {
    var router = Router.builder()
        .get("/", _ -> Response.ok("Hello, Chappe!"))
        .get("/users/{id}", req -> Response.ok("User " + req.pathParams().get("id")))
        .build();

    try (var server = Server.builder()
            .port(8080)
            .handler(router)
            .build()) {
        server.start();
        Thread.currentThread().join();
    }
}

Server is AutoCloseable; start() opens the listening socket and spawns the accept loop on a virtual thread.

void main() (no String[] args) is valid Java 25 thanks to JEP 512 (Compact Source Files). For a standard entry point, write public static void main(String[] args).

First typed handler

To read a JSON body, write a POST, and return an explicit status:

import io.vidocq.chappe.api.*;
import java.nio.charset.StandardCharsets;

Router api = Router.builder()
    .post("/users", req -> {
        var body = new String(
            req.body().asInputStream().readAllBytes(),
            StandardCharsets.UTF_8
        );
        return Response.builder()
            .status(StatusCode.CREATED)
            .header("Content-Type", "application/json")
            .body("{\"received\": " + body + "}")
            .build();
    })
    .delete("/users/{id}", req -> Response.of(StatusCode.NO_CONTENT))
    .build();

Serving a static directory

var staticHandler = StaticFileHandler.builder()
    .addPath(java.nio.file.Path.of("./public"))
    .addClasspath("static")
    .cacheInMemory(true)
    .cacheControl("max-age=3600, public")
    .build();

Server.builder().port(8080).handler(staticHandler).build().start();

StaticFileHandler auto-detects MIME (26+ types), answers 304 Not Modified on If-Modified-Since or If-None-Match, and blocks path traversal.

Build and run

sdk env                        # loads JDK 25 + Maven 3.9.16
./mvnw -ntp install -DskipTests
java \
     --module-path target/lib:target/myapp.jar \
     --module myapp/io.example.Main

Smoke test with wrk

Once the server is listening on 8080, confirm it sustains a minimal load:

wrk -t4 -c100 -d10s http://localhost:8080/

For reference numbers compared against Jetty, Helidon, and JDK HttpServer, see lien:https://codeberg.org/Vidocq/chappe/blob/main/BENCH.md[BENCH.md].

Diagram: starting a server

Diagram

Next steps

  • Usage — TLS, HTTP/2, content negotiation, virtual hosts, CLI.

  • ConceptsRequestContext, Filter, Body, mount.

  • Internals — HTTP/1.1, HTTP/2, HTTP/3 pipelines.