An opinionated, modern starter for building server-rendered web applications with Spring Boot, Thymeleaf, HTMX, _hyperscript, and Tailwind CSS — without the SPA tax.
bash <(curl -fsSL https://raw.githubusercontent.com/havlli/bootleaf-starter/master/scripts/create.sh) my-appThat single line clones, scaffolds (your group/artifact/version), installs the dev runner, and gives you a fresh git history. See One-command bootstrap below for the non-interactive variant and full flag list.
| Layer | Technology |
|---|---|
| Backend | Spring Boot 4.0 on Java 21 (virtual threads on) |
| HTMX bridge | htmx-spring-boot 5.1 (HtmxResponse, headers) |
| Toolchain | Pinned via mise (mise.toml): Temurin 21, Maven 3.9, Node 24 |
| Templates | Thymeleaf with vanilla parameterised-fragment layouts |
| Interactivity | HTMX 2.0 + _hyperscript 0.9 |
| Styling | Tailwind CSS v4 (CSS-first config, standalone CLI) |
| Validation | Jakarta Bean Validation (server-side, surfaced via HTMX retarget/reswap) |
| Dev pipeline | Node.js 24 LTS + browser-sync for live reload |
| Observability | Spring Boot Actuator (/actuator/health, /actuator/info with custom app metadata) |
| Quality gate | JUnit 5 + MockMvc + RestTestClient, Jacoco with 70% line-coverage rule, GitHub Actions CI |
- Layout-first templates — vanilla Thymeleaf parameterised fragments (
templates/layouts/main.html); no third-party layout dialect (which is currently incompatible with Spring Boot 4 / Groovy 5). - HTMX patterns showcase at
/patterns: server-side validation with retarget/reswap, click counter, inline edit, toast notifications viaHX-Trigger. - Themed error pages —
templates/error/404.htmlandtemplates/error/5xx.htmlreuse the brand layout;BasicErrorControllerresolves them automatically. - Zero-config dark mode with
prefers-color-scheme+ a click-to-toggle button (persisted inlocalStorage). - HTTP/2, response compression, and content-versioned static assets turned on by default in
application.properties. - Virtual threads enabled (
spring.threads.virtual.enabled=true) for free request throughput on Java 21. - Comprehensive test suite — controller slice tests, validation unit tests, full-stack integration tests with
RestTestClient. 30+ tests cover the golden path and HTMX edge cases. - One-command bootstrap (
bash <(curl …)/make create/./prepare) that renames packages, rewritespom.xml, reinitialises git, and runsmvnw verifyin one shot — with--template api-onlyfor a JSON-only variant. - CI out of the box — GitHub Actions workflow runs build, tests, Jacoco, and uploads coverage; Dependabot keeps Maven, Actions, and npm deps fresh.
- Git
- A version manager that reads
mise.toml— mise recommended (also works withasdf)- …or just install JDK 21, Maven 3.9+, and Node 24 LTS manually
The exact toolchain (JDK / Maven / Node versions) is pinned in mise.toml. With mise installed:
mise trust # one-time, after cloning
mise install # provisions Temurin 21, Maven 3.9.15, Node 24.15.0mise will then auto-set JAVA_HOME, MAVEN_HOME, and PATH whenever you cd into the project. The Maven wrapper (./mvnw) and frontend-maven-plugin continue to work as a fallback for contributors who don't use a version manager — frontend-maven-plugin will download Node 24 / npm 11 on demand.
The fastest path: a single line that clones, scaffolds, installs the root npm runner, and git inits a fresh history. Interactive (asks for groupId / artifactId / etc.):
bash <(curl -fsSL https://raw.githubusercontent.com/havlli/bootleaf-starter/master/scripts/create.sh) my-appFully non-interactive — every prompt answered up front:
bash <(curl -fsSL https://raw.githubusercontent.com/havlli/bootleaf-starter/master/scripts/create.sh) my-app \
--yes --template api-only --no-codecov \
--group-id com.acme --artifact-id myapp --version 1.0.0 \
--name "My App" --github-owner acme --github-repo myappWhen you're done you have a green-build project: cd my-app && npm run dev.
Prefer not to pipe
curlinto a shell?git clonethenbash scripts/create.sh ../my-app …does the same thing locally; or use the manual flow below.
Run scaffolding in a plain terminal before opening the project in an IDE — IntelliJ will eagerly write
.idea/metadata that fights the rename.
git clone https://github.com/havlli/bootleaf-starter.git your-new-project-name
cd your-new-project-name
./prepare # interactive
# or, fully non-interactive:
./prepare --yes \
--group-id com.acme --artifact-id widget --version 1.0.0 \
--name "Widget Service" --github-owner acme --github-repo widgetThe scaffolder (scripts/scaffold.mjs, Node 20+) is idempotent, dry-runnable, and rewrites everything in one shot:
| Flag | Effect |
|---|---|
--dry-run |
Print every move/write — change nothing |
--keep-git |
Rename project but preserve existing .git history (no fresh init) |
--skip-verify |
Skip the trailing ./mvnw verify |
--skip-badge-rewrite |
Leave README badge URLs pointing at havlli/bootleaf-starter |
--no-codecov |
Strip the Codecov badge from README (use until you wire Codecov) |
--template <kind> |
fullstack (default) or api-only (no Thymeleaf/HTMX/Tailwind/Node) |
--yes |
Non-interactive; combine with --group-id, --artifact-id, etc. |
--help |
Show usage and exit |
What gets rewritten: pom.xml coordinates (never dependency coords), .run/Application.run.xml, source + test packages, application*.properties, and the README's title + GitHub badge URLs (so acme/widget shows green CI / Codecov badges immediately after you push).
--template api-only additionally strips the frontend pipeline: removes Thymeleaf, htmx-spring-boot, the frontend-maven-plugin, the node/, templates/, and static/ folders, and drops the view classes/tests in favour of a minimal /api/ping REST controller and @WebMvcTest slice.
After a successful run the scaffolder removes prepare*, scripts/create.sh, and scripts/scaffold.mjs itself, so the new project doesn't carry the bootstrap machinery. The legacy prepare, prepare.sh, and prepare.cmd files are thin shims that forward all flags to the Node scaffolder, so muscle memory still works.
npm install # one-time, installs concurrently as a root dev-dep
npm run dev # Spring Boot (local profile) + Tailwind/cpx2/browser-sync, side by sideBrowse to http://localhost:3000 — browser-sync proxies to Spring on :8080 and reloads the page whenever a watched file changes. Ctrl-C once stops both processes.
Run configurations live under .run/. Use the compound spring & npm to start Spring Boot and the watcher in one click.
.vscode/launch.json ships a Java debug profile (Spring local profile pre-set) and .vscode/tasks.json wires npm run dev, verify, test, and the OCI image build into the command palette. .vscode/extensions.json recommends the Java/Spring/Tailwind/HTMX extension bundle on first open.
./mvnw spring-boot:run -Dspring-boot.run.profiles=localcd node && npm run dev./mvnw -Prelease clean packageThe release profile runs npm run prod, which builds CSS through the Tailwind v4 CLI with --minify. The default verify lifecycle also runs Jacoco and enforces a 70% line-coverage rule (excluding Application.class).
The Spring Boot Maven plugin can build an OCI image without a Dockerfile:
./mvnw spring-boot:build-imageFor collaborators without JDK 21 on their machine — or to demo the patterns page behind a real reverse proxy:
make up # builds the jar if needed, runs `docker compose up -d --build`
# browse to http://localhost:8080 (override with HTTP_PORT=9000 make up)
make logs # tail compose logs
make down # tear downcompose.yaml ships a two-service stack: the Spring Boot app (built via an inline Dockerfile from target/*.jar) plus a Caddy reverse proxy that gzip/zstd-encodes responses and adds Cache-Control: immutable headers for content-versioned static assets. The Spring service exposes an Actuator-backed health check; Caddy waits for it to go green before accepting traffic.
A thin Makefile wraps the most common verbs so non-npm folks (and CI scripts) can run things without remembering Maven goal flags:
make # list all targets
make dev # npm run dev (Spring + Tailwind + browser-sync)
make test # ./mvnw test
make verify # ./mvnw verify (Jacoco gate)
make image # OCI image via spring-boot:build-image
make scaffold # interactive ./prepare
make hooks # install lefthook git hooksOptional but recommended. lefthook.yml wires:
- pre-commit (fast):
./mvnw test-compile(offline, parallel) + a trailing-whitespace check on*.properties. - pre-push (slow): full
./mvnw verifyso the local gate matches CI.
brew install lefthook # or: go install github.com/evilmartians/lefthook@latest
make hooks # installs the .git/hooks shimssrc/main/
├── java/.../
│ ├── Application.java
│ ├── config/
│ │ └── RequestLoggingConfig.java # @Profile("local") request logging filter
│ ├── controller/
│ │ ├── HomeController.java # GET /, GET /patterns
│ │ └── WebController.java # /submit, /patterns/* HTMX endpoints
│ └── web/
│ └── MessageForm.java # Jakarta validation record
└── resources/
├── META-INF/
│ └── additional-spring-configuration-metadata.json # custom info.app.* keys
├── application.properties
├── application-local.properties
├── static/ # favicon, images, js/, vendored htmx + _hyperscript
└── templates/
├── index.html
├── error.html # generic fallback
├── error/
│ ├── 404.html # resolved by BasicErrorController on 404
│ └── 5xx.html # resolved on 500-class errors
├── fragments/ # reusable HTMX response fragments
│ ├── chip.html
│ ├── click.html
│ ├── error-card.html
│ ├── note.html
│ └── validate-form.html
├── layouts/
│ └── main.html # parameterised layout(title, head, content)
└── pages/
└── patterns.html # HTMX patterns showcase
src/test/java/.../
├── ApplicationTests.java # Spring context smoke test
├── ErrorPagesIntegrationTest.java # full-stack via RestTestClient
├── controller/
│ ├── HomeControllerTest.java # @WebMvcTest slice
│ └── WebControllerTest.java # HTMX retarget/trigger header assertions
└── web/
└── MessageFormValidationTest.java # pure Jakarta Validator tests
node/
├── package.json # @tailwindcss/cli, cpx2, browser-sync, npm-run-all2
├── styles.css # Tailwind v4 entry: @import + @theme + @utility
└── setup-dirs.js
| Path | Description |
|---|---|
/ |
Landing page |
/patterns |
HTMX patterns showcase: validation, click counter, inline edit |
/submit |
HTMX endpoint — returns the chip fragment |
/patterns/click |
HTMX POST — increments shared counter, returns updated fragment |
/patterns/validate |
HTMX POST — server-side validation with HX-Retarget on failure |
/patterns/note/{id} |
HTMX GET/POST — inline-edit pattern for a note store |
/actuator/health |
Health probe (Kubernetes-friendly) |
/actuator/info |
Build & app metadata (custom info.app.* keys) |
Out of the box this starter ships no authentication, no authorization, and no CSRF protection — Spring Security is intentionally not on the classpath, because adding it without an opinion (form login? OAuth2? JWT?) would force the wrong choice on you. The HTMX patterns page exists to demo client-server interactions, not to be deployed user-facing as-is.
Before exposing this to the public internet:
- Add
spring-boot-starter-securityand configure CSRF (the HTMX form posts will need the_csrftoken surfaced into headers —htmx-spring-boothas helpers for this). - Lock down the actuator:
management.endpoints.web.exposure.includeis currentlyhealth,info; review the Spring Boot Actuator docs before exposing more. info.app.*keys are surfaced via/actuator/info— keep nothing sensitive inapplication.propertiesunder that prefix.- Configure rate limiting / request-size limits / CORS at the reverse-proxy layer (Caddy in
compose.yaml, or your platform).
./mvnw verifyruns unit + integration tests, then the Jacoco line-coverage rule (≥ 70%, excludingApplication.class).- The GitHub Actions workflow at
.github/workflows/ci.ymlruns the same on every push and PR, uploads the Jacoco HTML report as an artifact, and pushes coverage to Codecov. - Dependabot opens grouped PRs weekly for Maven (Spring + testing groups) and monthly for GitHub Actions and npm.
Honest list of what could still be smoother — open to PRs:
- Test data builders. The
MessageFormvalidation is well-covered, but as the project grows a*Fixturesbuilder pattern (orinstancio) keeps test setup terse. - Formatter in pre-commit. Lefthook currently runs
test-compile+ a properties-whitespace check; addingpalantir-java-format(Java) andprettier(templates/CSS) would normalise style end-to-end. - JetBrains Fleet run configs. IntelliJ
.run/and VS Code.vscode/ship; Fleet still needs a hand-rolled task graph. - More
--templatepresets. Beyondfullstackandapi-only, aworker(no web stack, just@Scheduledjobs + Actuator) preset would be useful for batch services.
Contributions are welcome. Fork, branch, and open a pull request — the CI workflow will run automatically.