DEV Community

Alex Spinov
Alex Spinov

Posted on

htmx Has a Free API — Build Interactive Apps Without JavaScript

htmx is the HTML-over-the-wire library that lets you build modern, interactive web apps with just HTML attributes — no JavaScript framework needed. 35K+ GitHub stars and growing.

Why htmx?

  • No build step — just a script tag
  • No JavaScript — HTML attributes for AJAX, WebSockets, SSE
  • Works with any backend — Django, Rails, Express, Go, PHP
  • 12KB gzipped — smaller than a React hello world
  • Hypermedia-driven — the server returns HTML, not JSON

Quick Start

<script src="https://unpkg.com/htmx.org@2.0.4"></script>
Enter fullscreen mode Exit fullscreen mode

That's it. No npm install. No webpack. No bundler.

Core Concepts

<!-- Click button → GET /api/users → insert response into #user-list -->
<button hx-get="/api/users" hx-target="#user-list" hx-swap="innerHTML">
  Load Users
</button>
<div id="user-list"></div>

<!-- Submit form → POST /api/contacts → append to list -->
<form hx-post="/api/contacts" hx-target="#contacts" hx-swap="beforeend">
  <input name="name" placeholder="Name" />
  <input name="email" placeholder="Email" />
  <button type="submit">Add Contact</button>
</form>
<div id="contacts"></div>
Enter fullscreen mode Exit fullscreen mode

Search (Live, No JavaScript)

<input
  type="search"
  name="q"
  placeholder="Search users..."
  hx-get="/api/search"
  hx-trigger="keyup changed delay:300ms"
  hx-target="#results"
/>
<div id="results"></div>
Enter fullscreen mode Exit fullscreen mode

Server endpoint returns HTML:

# Flask example
@app.get("/api/search")
def search():
    q = request.args.get("q", "")
    users = User.query.filter(User.name.ilike(f"%{q}%")).all()
    return render_template("partials/user_list.html", users=users)
Enter fullscreen mode Exit fullscreen mode

Infinite Scroll

<div id="feed">
  <!-- Existing items -->
  <div class="post">Post 1</div>
  <div class="post">Post 2</div>

  <!-- This triggers when scrolled into view -->
  <div hx-get="/api/posts?page=2"
       hx-trigger="revealed"
       hx-swap="afterend">
    Loading...
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Delete with Confirmation

<button
  hx-delete="/api/users/123"
  hx-confirm="Are you sure you want to delete this user?"
  hx-target="closest tr"
  hx-swap="outerHTML swap:500ms"
>
  Delete
</button>
Enter fullscreen mode Exit fullscreen mode

The row fades out and is removed from the DOM — no JavaScript.

Inline Editing

<!-- Display mode -->
<div id="user-name" hx-get="/api/users/1/edit" hx-trigger="click" hx-swap="outerHTML">
  <span>Alice</span>
  <small>(click to edit)</small>
</div>

<!-- Server returns edit form -->
<!-- /api/users/1/edit returns: -->
<form hx-put="/api/users/1" hx-target="this" hx-swap="outerHTML">
  <input name="name" value="Alice" />
  <button type="submit">Save</button>
  <button hx-get="/api/users/1" hx-target="this" hx-swap="outerHTML">Cancel</button>
</form>
Enter fullscreen mode Exit fullscreen mode

WebSocket Support

<div hx-ws="connect:/ws/chat">
  <div id="messages"></div>

  <form hx-ws="send">
    <input name="message" placeholder="Type a message..." />
    <button type="submit">Send</button>
  </form>
</div>
Enter fullscreen mode Exit fullscreen mode

Server-Sent Events

<div hx-sse="connect:/api/notifications">
  <div hx-sse="swap:notification" id="alerts">
    <!-- New notifications appear here automatically -->
  </div>

  <div hx-sse="swap:progress">
    <!-- Progress updates stream here -->
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Loading Indicators

<button hx-get="/api/report" hx-target="#report">
  Generate Report
  <span class="htmx-indicator">
    <img src="/spinner.svg" /> Loading...
  </span>
</button>
Enter fullscreen mode Exit fullscreen mode

The indicator shows automatically during the request and hides when complete.

Express.js Backend Example

import express from "express";

const app = express();
app.use(express.urlencoded({ extended: true }));

let todos = [
  { id: 1, text: "Learn htmx", done: false },
];

app.get("/", (req, res) => {
  res.send(`
    <html>
    <head>
      <script src="https://unpkg.com/htmx.org@2.0.4"></script>
    </head>
    <body>
      <h1>Todo App</h1>
      <form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend" hx-on::after-request="this.reset()">
        <input name="text" placeholder="New todo..." required />
        <button type="submit">Add</button>
      </form>
      <ul id="todo-list">
        ${todos.map(t => `<li>${t.text}</li>`).join("")}
      </ul>
    </body>
    </html>
  `);
});

app.post("/todos", (req, res) => {
  const todo = { id: Date.now(), text: req.body.text, done: false };
  todos.push(todo);
  // Return just the new HTML fragment
  res.send(`<li>${todo.text}</li>`);
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Full interactive todo app. Zero client-side JavaScript written.

htmx vs React vs Alpine.js vs Stimulus

Feature htmx React Alpine.js Stimulus
Size 12KB 40KB+ 15KB 20KB
Build step None Required None Optional
Learning curve HTML only JSX + hooks HTML + JS HTML + JS
Server rendering Native SSR (complex) No Turbo
JavaScript needed None Yes Minimal Minimal
State management Server Client Client Both
Best for Server-rendered apps SPAs Sprinkles Rails apps

Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.

Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.

Top comments (0)