A blazing-fast, reactive Single Page Application (SPA) built with Jooby, HTMX, and Handlebars.
This application demonstrates the power of the jooby-htmx module, delivering a complex, interactive user interface—including drag-and-drop sorting, dynamic counters, and toast notifications—without writing complex client-side JavaScript.
- Java 21 or newer
- Maven
- Clone the repository.
- Compile and run the application:
mvn jooby:run
- SPA Shell Navigation: Full-page reloads on direct browser access, but lightning-fast partial fragment swaps during HTMX navigation.
- Declarative UI Updates: Adding a task updates the list, increments the global task counter, fires a JS event, and displays a success toast simultaneously.
- Scoped Error Handling: Inline form validation (HTTP 422) that catches errors, displays them next to the form, and automatically clears them upon a successful submission.
- Drag-and-Drop Reordering: Persists list order to the backend and triggers OOB (Out-Of-Band) toast notifications upon success.
This project leverages the first-class HTMX annotations provided by Jooby. Here is a look at the core mechanics powering the app:
The application uses the @HxView annotation to automatically determine if a user is doing a full page load (F5/Bookmark) or an AJAX request.
@GET("/tasks")
@HxView(value = "board.hbs", layout = "index.hbs")
public TaskBoard getBoard() {
return db.getBoardState();
}-
Browser Request: Injects board.hbs into the
index.hbslayout shell. -
HTMX Request: Returns only the
board.hbsfragment.
When a user adds a task, we need to update multiple parts of the screen at once. Instead of writing custom JSON endpoints and client-side state management, we just declare our UI targets:
@POST("/tasks")
@HxView("task_row.hbs") // 1. The main response (the new row)
@HxOob("task_counter.hbs") // 2. Update the total count badge
@HxOob("toast.hbs") // 3. Show a success popup
@HxTrigger("taskAdded") // 4. Fire a JS event for animations
public Task addTask(@Valid TaskDto dto) {
var newTask = db.save(dto);
return newTask;
}By annotating the controller with @HxError("task_error.hbs"), any Jakarta @Valid failures automatically render an inline error message next to the form.
When the user fixes the error and submits successfully, the framework automatically appends an empty task_error.hbs OOB swap to seamlessly wipe the error from the screen.
For endpoints that don't need to return a main view (like deleting or reordering tasks), the app uses the HtmxResponse builder to explicitly trigger updates:
@DELETE("/tasks/{id}")
public HtmxResponse deleteTask(@PathParam String id) {
db.delete(id);
return HtmxResponse.empty()
.addOob("task_counter.hbs", Map.of("activeCount", db.getActiveCount()))
.addOob("toast.hbs", Map.of("message", "Task deleted!"));
}