# thymeleaf-htmx Companion project for **Spring Boot 4 + Thymeleaf + htmx: Server-Side CRUD UI in 2026** on **[ankurm.com](https://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 ```bash 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.