Handlers
Handler has one method, so how a handler is written is the application’s call rather than the framework’s.
The example app uses all three shapes.
A lambda
When there is no state worth a class:
app.get("/openapi.json", req -> WebResponse.json(OpenApi.document(app.routes())));
An Action class
When a class answers exactly one route.
It implements Handler, so it registers as itself:
public class StatsAction implements Handler {
private final StatsService statsService;
public StatsAction(StatsService statsService) {
this.statsService = statsService;
}
@Override
public WebResponse handle(WebRequest req) {
return WebResponse.template("stats", Map.of("stats", statsService.overview()));
}
}
app.get("/stats", context.statsAction());
Action is the A of the ADR (Action-Domain-Responder) pattern: a web-tier class that answers exactly one request, the same convention as Laravel’s single action controllers.
The framework does not know the name: Handler is the interface, …Action is a naming convention for the classes that implement it directly.
Public methods, registered by reference
When one class answers several related routes:
public class DeckController {
public WebResponse showDeck(WebRequest req) { ... }
public WebResponse renameDeck(WebRequest req) { ... }
}
DeckController decks = context.deckController();
app.get("/decks/{deckId}", decks::showDeck);
app.post("/decks/{deckId}/rename", decks::renameDeck);
The methods are public because registration happens outside the class, and that is the point.
There is no Controller interface and no register(App) method to implement, so every route the application answers is one statement in one list, and app.routes() reports exactly what is written there.
A class that registers its own routes hides half the routing table inside itself, which is the thing annotation scanning does, just spelled out by hand.