Spider Silk

Spider Silk

Thin by design, strong by types.

Spider Silk is a web framework built on the Jakarta Servlet API.

Three core principles

No reflection.

There is no annotation scanning, no proxies, and no automatic binding. What runs is what the code says, stack traces stay short, and startup stays fast.

The API is intuitively simple.

A handler is a function from a request to a response: WebResponse handle(WebRequest request). That signature is the whole model.

Better RESTful API support than raw servlets.

Routing is per method, paths carry variables, parameters are extracted with a declared type, and exceptions map to status codes.

The default template engine is jte. jte compiles templates to Java source, which matches how the rest of the framework works.

Modules

Every module depends on spider-silk-core, and core depends on none of them. A module is named after what it is tied to: a server, a template engine, or Jetty’s WebSocket.

Every module depends on spider-silk-core; the example application depends on core and openapi
Figure 1. The modules and what they depend on
Module Contents Dependencies

spider-silk-core

The framework itself

gg.jte:jte, embedded Jetty (jetty-ee10-servlet)

spider-silk-test

The WebTest harness and TestRequest, for test scope

core, and otherwise the JDK only (the servlet API compile-time only, as in core)

example-flashcard

Example: a flashcard study app

core, spring-jdbc, H2

At a glance

App app = new App();    // jte over classpath:/jte, and classpath:/public served at /

app.get("/decks/{deckId}", req -> {
    long deckId = req.pathParamLong("deckId");  // non-numeric input becomes a 400
    return WebResponse.template("deck", model); // classpath:/jte/deck.jte
});

app.get("/api/decks", req -> {
    Json.JsonArray resBody = Json.arr().add(
        Json.obj()
            .put("id", 1L)
            .put("name", "English")
    );
    return WebResponse.json(resBody);
});

app.post("/api/decks", req -> {
    String name = req.bodyJson().asObject().getString("name");
    Json.JsonObject resBody = Json.obj().put("name", name);
    return WebResponse.json(resBody).status(HttpStatus.CREATED);
});

app.path("/api/decks", group -> {
    group.beforeRoute(req -> requireApiKey(req));    // guards matched routes under /api/decks
    group.get("", api::listDecks);                   // GET  /api/decks
    group.get("/{deckId}", api::showDeck);           // GET  /api/decks/{deckId}
});

app.exception(IllegalArgumentException.class,
        (req, e) -> WebResponse.text(e.getMessage()).status(HttpStatus.NOT_FOUND));
app.exception(Json.JsonException.class,        // the more specific type wins, whatever the order
        (req, e) -> WebResponse.text(e.getMessage()).status(HttpStatus.BAD_REQUEST));

app.error(HttpStatus.NOT_FOUND, req -> WebResponse.template("not-found", Map.of("path", req.path())));

app.start(8080);                                // embedded Jetty, sessions on

start returns as soon as the port is bound. Everything is registered before it: a route, a filter, or a setting added while the application is served throws IllegalStateException, and stop() opens registration again. The server’s threads keep the JVM alive from there, so join() is only needed when you want to block the main thread as well. stop() shuts the server down. port() reports the bound port, which is how a test reads back the port the OS picked for start(0).

Positioning and roadmap

notes/positioning.md places Spider Silk next to Javalin, Spark, Helidon SE, and Spring Boot, and states what it trades away to get there.

notes/decisions.md gives the reasoning behind each piece, item by item, together with the list of what was rejected.

The issue tracker records what was deliberately deferred, one issue per item, with the condition that would make it worth doing.