Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
thymeleaf-htmx
Companion project for Spring Boot 4 + Thymeleaf + htmx: Server-Side CRUD UI in 2026 on ankurm.com.
A contacts list with search, pagination, create-with-validation, inline edit and delete. All HTML is rendered by Thymeleaf; htmx only decides which part of a page to replace. About 25 lines of JavaScript-free attributes, no JS framework, no build step.
Every console block in the article is quoted from output/. The MockMvc transcripts are written by the tests themselves (they assert on the same data), and output/11-browser-check.txt comes from headless Chromium driving the packaged jar.
Versions
| Spring Boot | 4.1.2 (latest GA on Maven Central; 4.2.0-RC1 exists but is not GA) |
| Thymeleaf | 3.1.5.RELEASE (managed by Boot) |
| htmx | 2.0.11 as the WebJar org.webjars.npm:htmx.org (htmx 4.0.0 exists on npm next; see the article) |
| JDK | 25 |
| Test tooling | spring-boot-starter-webmvc-test, spring-boot-starter-thymeleaf-test; Playwright 1.64.1 for the browser check (optional) |
Run
export JAVA_HOME=/path/to/jdk-25
mvn spring-boot:run # http://localhost:8080/contacts
mvn test # 10 MockMvc tests, rewrite output/01..09
./scripts/run-all.sh # tests + live curl + browser check, regenerates all of output/
The browser check needs Node 20+ and a one-time npx playwright install chromium.
Where things are
| Concern | File |
|---|---|
Layout fragment (layout(title, content)), htmx config meta tag, WebJar script tag |
templates/layout/base.html |
List page + the table fragment htmx receives |
templates/contacts/list.html |
| Row, edit-row (also the error state) | templates/contacts/fragments.html |
Create form (full page and form fragment) |
templates/contacts/new.html |
Fragment vs full page decision (HX-Request, HX-History-Restore-Request) |
Htmx.java, ContactController.java |
Vary header |
VaryHtmxFilter.java |
| Contract tests | ContactUiTest.java, Transcript.java |
| Real-browser behaviour checks | browser-check/check.mjs |
Output
| File | Shows |
|---|---|
output/00-mvn-test.txt |
surefire summary |
output/01-full-page-vs-fragment.txt |
same URL, with and without HX-Request |
output/02-history-restore-gets-full-page.txt |
the history-miss case |
output/03-htmx-webjar.txt |
version-less WebJar path resolved to the pinned version |
output/04-pagination-and-search.txt |
last page, page past the end, search |
output/05-create-validation-errors.txt |
422 with errors, fragment and full page |
output/06-create-success.txt |
HX-Redirect vs 302 |
output/07-inline-edit.txt |
edit row, failed save, successful save |
output/08-delete.txt |
200 + HX-Trigger, 404 |
output/09-delete-last-page-clamps.txt |
pager after deleting the last page's rows |
output/10-live-curl.txt |
real Tomcat answering with curl |
output/11-browser-check.txt |
Chromium: swaps, history, 204 and 422 traps |
Not covered: Spring Security / CSRF (no Security dependency in this module), file uploads, and htmx 4.