---
name: htmx-spring-boot
description: HTMX + Spring Boot 2.6 integration with Thymeleaf. Use when building server-side rendered web apps with dynamic AJAX interactions, handling HTMX requests/responses, implementing CRUD operations with minimal JavaScript, or working with Thymeleaf fragments and HTMX attributes.
---

# HTMX + Spring Boot Integration

Comprehensive guide for building modern, interactive web applications using HTMX with Spring Boot 2.6+ and Thymeleaf templates.

## Core Capabilities

### 1. HTMX Request Detection

Use `@HxRequest` annotation to create endpoints specifically for HTMX requests:

```java
@HxRequest
@GetMapping("/users")
public String users() {
    return "fragments/user-list"; // Returns HTML fragment
}

// Conditional based on trigger element
@HxRequest("edit-button")
@GetMapping("/users/{id}/edit")
public String editUser(@PathVariable Long id) {
    return "fragments/user-edit-form";
}

// Conditional based on target element
@HxRequest(target = "user-details")
@GetMapping("/users/{id}")
public String userDetails(@PathVariable Long id) {
    return "fragments/user-details";
}
```

### 2. HTMX Response Headers

Set HTMX response headers using annotations or `HtmxResponse`:

```java
// Using annotations
@HxRequest
@HxTrigger("user-updated")
@HxRetarget("#notification")
@PostMapping("/users/{id}")
public String updateUser(@PathVariable Long id, @Valid UserForm form) {
    // Update logic
    return "fragments/success-message";
}

// Using HtmxResponse for dynamic control
@HxRequest
@PostMapping("/users")
public String createUser(@Valid UserForm form, HtmxResponse response) {
    if (isDuplicate(form.getEmail())) {
        response.setRetarget("#error-container");
        response.setReswap(HtmxReswap.beforeend());
        return "fragments/error-message";
    }

    response.addTrigger("user-created");
    response.setPushUrl("/users/" + newUser.getId());
    return "fragments/user-card";
}
```

### 3. Thymeleaf + HTMX Integration

Use `hx:` namespace for dynamic attributes in Thymeleaf:

```html
<!-- Dynamic hx-get with URL building -->
<button hx:get="@{/users/{id}(id=${user.id})}" hx-target="#details">
  View Details
</button>

<!-- Dynamic hx-vals with Thymeleaf expressions -->
<form hx-post="/search" hx:vals="${{query: searchTerm, category: category.id}}">
  <button type="submit">Search</button>
</form>

<!-- Conditional attributes -->
<div th:if="${user.isActive()}" hx-get="/users/active" hx-trigger="every 30s">
  Active Users
</div>
```

### 4. CRUD Patterns with HTMX

**Create (POST):**

```java
@HxRequest
@PostMapping("/items")
public HtmxResponse createItem(@Valid ItemForm form, Model model) {
    Item item = itemService.create(form);
    model.addAttribute("item", item);

    return HtmxResponse.builder()
        .view("fragments/item-row")
        .trigger("item-created")
        .build();
}
```

```html
<form
  hx-post="/items"
  hx-target="#item-list"
  hx-swap="beforeend"
  hx-on::after-request="this.reset()"
>
  <input name="name" required />
  <button type="submit">Add Item</button>
</form>
```

**Read (GET):**

```java
@HxRequest
@GetMapping("/items/{id}")
public String getItem(@PathVariable Long id, Model model) {
    model.addAttribute("item", itemService.findById(id));
    return "fragments/item-details";
}
```

**Update (PUT):**

```java
@HxRequest
@PutMapping("/items/{id}")
public HtmxResponse updateItem(@PathVariable Long id,
                               @Valid ItemForm form) {
    Item updated = itemService.update(id, form);

    return HtmxResponse.builder()
        .view(new ModelAndView("fragments/item-row",
              Map.of("item", updated)))
        .trigger("item-updated")
        .build();
}
```

**Delete (DELETE):**

```java
@HxRequest
@DeleteMapping("/items/{id}")
public HtmxResponse deleteItem(@PathVariable Long id) {
    itemService.delete(id);

    return HtmxResponse.builder()
        .trigger("item-deleted")
        .build(); // Empty response removes element
}
```

```html
<button
  hx-delete="/items/{id}"
  hx-target="closest tr"
  hx-swap="outerHTML swap:0.5s"
  hx-confirm="Delete this item?"
>
  Delete
</button>
```

### 5. Out-of-Band Swaps

Update multiple parts of the page in one response:

```java
@HxRequest
@PostMapping("/items")
public View createItem(@Valid ItemForm form, Model model) {
    Item item = itemService.create(form);
    long count = itemService.count();

    model.addAttribute("item", item);
    model.addAttribute("count", count);

    return FragmentsRendering
        .with("fragments/item-row")      // Main target
        .fragment("fragments/item-count")  // OOB swap
        .build();
}
```

```html
<!-- Main target -->
<div id="item-list" hx-target="this" hx-swap="beforeend">
  <!-- New item inserted here -->
</div>

<!-- OOB target -->
<span id="item-count" hx-swap-oob="true">
  <span th:text="${count}">0</span> items
</span>
```

### 6. Form Validation

Server-side validation with HTMX:

```java
@HxRequest
@PostMapping("/users")
public String createUser(@Valid @ModelAttribute UserForm form,
                        BindingResult result,
                        Model model) {
    if (result.hasErrors()) {
        model.addAttribute("form", form);
        return "fragments/user-form"; // Re-render with errors
    }

    User user = userService.create(form);
    model.addAttribute("user", user);
    return "fragments/user-card";
}
```

```html
<form hx-post="/users" hx-target="#form-container" hx-swap="outerHTML">
  <div>
    <input
      name="email"
      th:field="*{email}"
      th:classappend="${#fields.hasErrors('email')} ? 'error'"
    />
    <span
      th:if="${#fields.hasErrors('email')}"
      th:errors="*{email}"
      class="error-message"
    ></span>
  </div>
  <button type="submit">Submit</button>
</form>
```

### 7. Inline Validation

Validate fields as user types:

```html
<input
  name="email"
  hx-post="/validate/email"
  hx-trigger="blur, keyup changed delay:500ms"
  hx-target="next .validation-message"
  hx-swap="innerHTML"
/>
<span class="validation-message"></span>
```

```java
@HxRequest
@PostMapping("/validate/email")
@ResponseBody
public String validateEmail(@RequestParam String email) {
    if (!emailValidator.isValid(email)) {
        return "<span class='error'>Invalid email format</span>";
    }
    return "<span class='success'>✓</span>";
}
```

### 8. Redirects and Navigation

Handle redirects in HTMX context:

```java
// Client-side redirect (full page reload)
@HxRequest
@PostMapping("/users/{id}")
public HtmxRedirectView updateUser(@PathVariable Long id,
                                   @Valid UserForm form) {
    userService.update(id, form);
    return new HtmxRedirectView("/users/" + id);
}

// Using view name prefix
@HxRequest
@PostMapping("/users/{id}")
public String updateUser(@PathVariable Long id,
                        @Valid UserForm form,
                        RedirectAttributes attrs) {
    userService.update(id, form);
    attrs.addFlashAttribute("message", "User updated");
    return "redirect:htmx:/users/" + id;
}

// Location redirect (AJAX navigation, no reload)
@HxRequest
@PostMapping("/users/{id}")
public HtmxLocationRedirectView updateUser(@PathVariable Long id) {
    userService.update(id);
    return new HtmxLocationRedirectView("/users/" + id);
}

// Refresh current page
@HxRequest
@PostMapping("/settings")
public HtmxRefreshView updateSettings(@Valid SettingsForm form) {
    settingsService.update(form);
    return new HtmxRefreshView();
}
```

### 9. CSRF Protection

Handle CSRF tokens with HTMX:

```html
<!-- Include meta tags in layout -->
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />

<!-- Configure HTMX globally -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    document.body.addEventListener("htmx:configRequest", function (evt) {
      const token = document
        .querySelector('meta[name="_csrf"]')
        .getAttribute("content");
      const header = document
        .querySelector('meta[name="_csrf_header"]')
        .getAttribute("content");
      evt.detail.headers[header] = token;
    });
  });
</script>
```

### 10. Error Handling

Global error handler for HTMX requests:

```java
@ControllerAdvice
public class HtmxErrorHandler {

    @ExceptionHandler(Exception.class)
    public HtmxResponse handleError(Exception ex,
                                    HtmxRequest htmxRequest) {
        if (htmxRequest.isHtmxRequest()) {
            return HtmxResponse.builder()
                .reswap(HtmxReswap.none())
                .retarget("#error-container")
                .view(new ModelAndView("fragments/error",
                      Map.of("message", ex.getMessage())))
                .build();
        }
        throw ex; // Handle non-HTMX requests normally
    }
}
```

## Spring Boot 2.6 Compatibility

**Dependencies (Maven):**

```xml
<dependency>
    <groupId>io.github.wimdeblauwe</groupId>
    <artifactId>htmx-spring-boot</artifactId>
    <version>3.6.2</version>
</dependency>
<dependency>
    <groupId>io.github.wimdeblauwe</groupId>
    <artifactId>htmx-spring-boot-thymeleaf</artifactId>
    <version>3.6.2</version>
</dependency>
```

**Key Constraints:**

- No Java text blocks (use string concatenation)
- No records (use classes with getters/setters)
- Spring Security 5.x (not 6.x)
- Thymeleaf 3.0.x

## Resources

- `references/spring-integration.md` - Controller patterns, annotations
- `references/thymeleaf-patterns.md` - Template integration
- `references/htmx-attributes.md` - Core HTMX attributes reference
- `references/security.md` - CSRF, authentication, Spring Security
- `assets/examples/` - Complete working examples
- `scripts/` - Utility scripts for validation and scaffolding
