HTMX
Build interactive applications with HTMX's HTML-over-the-wire approach.
Build Configuration
Add HTMX to your build by specifying the directory containing your HTMX pages:
1const manifest = await build({
2 htmxDirectory: 'src/htmx/pages'
3});Page Handler
Pass the path to the built HTML file to handleHTMXPageRequest:
1// backend/server.ts
2import { handleHTMXPageRequest } from '@absolutejs/absolute';
3
4new Elysia()
5 .get('/app', () =>
6 handleHTMXPageRequest('./build/pages/app.html')
7 )Example
HTMX pages use HTML attributes to trigger server requests and update the DOM:
1<!-- src/htmx/pages/app.html -->
2<!DOCTYPE html>
3<html lang="en">
4<head>
5 <title>HTMX App</title>
6 <script src="https://unpkg.com/[email protected]"></script>
7 <link rel="stylesheet" href="./styles/htmx-app.css">
8</head>
9<body>
10 <h1>HTMX Application</h1>
11
12 <button hx-get="/api/data" hx-target="#results">
13 Load Data
14 </button>
15
16 <div id="results"></div>
17
18 <script src="./scripts/htmx-helpers.ts"></script>
19</body>
20</html>API Endpoints
HTMX requests are handled by regular Elysia endpoints that return HTML fragments:
1// HTMX requests return HTML fragments
2new Elysia()
3 .get('/api/data', () => {
4 const items = getItems();
5
6 return `
7 <ul>
8 ${items.map(item => `<li>${item.name}</li>`).join('')}
9 </ul>
10 `;
11 })- Return HTML: Endpoints return HTML strings that HTMX injects into the page
- Type-safe data: Your data fetching is still fully typed even though the response is HTML
- No JSON serialization: Skip the JSON/parse cycle for simpler data flow
Per-User State with Scoped State
When building interactive HTMX applications, you often need state that's specific to each user. For example, a counter button should only increment that user's count, not everyone's. The elysia-scoped-state plugin solves this by automatically managing per-user sessions.
Without scoped state, all users would share the same server state. With scoped state, each user gets their own isolated state slice:
1import Elysia from 'elysia';
2import { scopedState } from 'elysia-scoped-state';
3import { handleHTMXPageRequest } from '@absolutejs/absolute';
4
5new Elysia()
6 .use(
7 scopedState({
8 count: { value: 0 },
9 cart: { value: [] }
10 })
11 )
12 .get('/app', () => handleHTMXPageRequest('./build/pages/app.html'))
13 .get('/api/count', ({ scopedStore }) => {
14 // Returns this user's count only
15 return `<span>${scopedStore.count}</span>`;
16 })
17 .post('/api/increment', ({ scopedStore }) => {
18 // Only increments this user's count
19 return `<span>${++scopedStore.count}</span>`;
20 })
21 .listen(3000);The HTML stays the same, but now each user's interactions only affect their own state:
1<!-- Each user's button clicks only affect their own count -->
2<div>
3 Count: <span id="count" hx-get="/api/count" hx-trigger="load">0</span>
4</div>
5
6<button hx-post="/api/increment" hx-target="#count" hx-swap="innerHTML">
7 +1
8</button>
9
10<!-- User A clicks 5 times → sees 5 -->
11<!-- User B visits the page → sees 0 (their own fresh state) -->
12<!-- User B clicks 2 times → sees 2 (independent from User A) -->- Automatic sessions: A secure
user_session_idcookie is created on first visit - User isolation: Each user's
scopedStoreis completely independent - No client-side state: All state lives on the server, perfect for HTMX's HTML-over-the-wire approach
See the Scoped State documentation for more details on configuration options and advanced usage.