DEV Community

木头人
木头人

Posted on

Ditch Electron: Zero-Build Server-Driven UI with HTMX & Robyn

Part 5 of the ERTH Architecture Series: Eliminating complex Javascript bundlers by rendering HTML fragments directly from Python inside a native WebView.


The Cover Page

In the fourth part of this series, we integrated Turso and SQLModel into our Python sidecar, establishing a local-first persistent data channel with offline capabilities and cloud sync.

Now, we have a secure backend running on a dynamic port, protected by a watchdog and backed by database replication.

The last remaining step is to build the user interface.

Traditionally, this means setting up Node.js, running npm install, configuring Vite or Webpack, choosing React or Vue, setting up state management (Redux/Zustand), and writing boilerplate JSON API endpoints just to pass data back and forth. This frontend tax slows down development and inflates app bundle sizes.

What if we could skip the build pipeline entirely? What if our Python backend could return HTML fragments directly to the WebView, updating the interface instantly without full page reloads?

In this final post, we will integrate HTMX into our ERTH Stack (ElectroBun + Robyn + Turso + HTMX) to achieve a Zero-Build, Server-Driven UI.


The Hypermedia Exchange

Rather than fetching JSON and parsing it on the client, HTMX allows us to make AJAX requests directly from HTML attributes. The server processes the request, renders an HTML fragment, and returns it. HTMX then swaps this fragment directly into the DOM.

Here is the hypermedia lifecycle:

HTMX Hypermedia Exchange

Let's build this.


Step 1: Writing Server-Driven UI in Python

In our Robyn backend, we implement a routing structure that supports Dual-Mode Responses. If a request comes from HTMX (indicated by the hx-request header), we render HTML. If it is a standard request, we return JSON (perfect for CLI testing and APIs).

Here is the task toggle route in Python:

# backend/app.py
from robyn import Robyn, Request, Response
from db import toggle_todo_status

app = Robyn(__file__)

def render_todo_item(todo: dict) -> str:
    """Renders a single todo list item as an HTML fragment"""
    completed_class = "completed" if todo["is_completed"] else ""
    checked_attr = "checked" if todo["is_completed"] else ""

    return f"""
    <div class="todo-item {completed_class}" id="todo-{todo['id']}">
        <input type="checkbox" 
               {checked_attr} 
               hx-put="/api/v1/todos/{todo['id']}/toggle"
               hx-target="#todo-{todo['id']}"
               hx-swap="outerHTML">
        <span>{todo['title']}</span>
    </div>
    """

@app.put("/api/v1/todos/:id/toggle")
async def toggle_todo(request: Request):
    todo_id = request.path_params.get("id")
    updated_todo = await toggle_todo_status(todo_id)

    if not updated_todo:
        return Response(status_code=404, description="Todo not found")

    is_htmx = request.headers.get("hx-request") == "true"
    if is_htmx:
        # Return pure HTML fragment for HTMX to swap in the DOM
        return Response(
            status_code=200,
            headers={"Content-Type": "text/html; charset=utf-8"},
            description=render_todo_item(updated_todo)
        )
    else:
        # Standard REST response
        import json
        return Response(
            status_code=200,
            headers={"Content-Type": "application/json"},
            description=json.dumps(updated_todo)
        )
Enter fullscreen mode Exit fullscreen mode

Step 2: Designing the Zero-Build Frontend

On the frontend side inside our ElectroBun container, our index.html is extremely simple. We link directly to the local htmx.min.js file. There are no build tools, loaders, or packagers.

<!-- views/main/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ERTH Assistant</title>
    <script src="js/htmx.min.js"></script>
    <style>
        .todo-item { display: flex; align-items: center; gap: 10px; padding: 8px; }
        .todo-item.completed span { text-decoration: line-through; color: #888; }
    </style>
</head>
<body>
    <h1>Tasks</h1>

    <!-- Loaded dynamically when the watchdog negotiates the dynamic port -->
    <div id="todo-list" hx-get="/api/v1/todos" hx-trigger="backend-ready from:body">
        <p>Connecting to backend engine...</p>
    </div>

    <script>
      // Automatically configure HTMX requests to inject security tokens and redirect to dynamic port
      document.addEventListener('htmx:configRequest', (evt) => {
        if (window.__ENV__ && window.__ENV__.BACKEND_PORT) {
          const port = window.__ENV__.BACKEND_PORT;
          if (evt.detail.path.startsWith('/')) {
            evt.detail.path = `http://127.0.0.1:${port}${evt.detail.path}`;
          }
        }
        if (window.__ENV__ && window.__ENV__.TOKEN) {
          evt.detail.headers['Authorization'] = `Bearer ${window.__ENV__.TOKEN}`;
        }
      });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

When the Bun process dispatches the backend-ready event, HTMX instantly triggers a GET request to retrieve the tasks, rendering them in place. When a user clicks a checkbox, HTMX sends a PUT request carrying the Opaque Token, and swaps the returned task layout in milliseconds—without re-rendering the entire page.


Recap of the ERTH Desktop Architecture

Across these five posts, we have constructed a new desktop architecture that challenges the monopoly of Electron and Tauri:

  1. E - ElectroBun: Provides our lightweight frontend shell. It replaces Electron's bloated Chromium engine with native system WebViews (WebKit), cutting base memory usage to just 15MB.
  2. R - Robyn: Launches an async Python sidecar on a dynamic Port 0. It runs Actix-web under the hood, running heavy business logic and enabling seamless integration with local AI toolkits (Ollama, PyTorch).
  3. T - Turso (libSQL): Serves as our Local-First synchronization database. Combined with SQLModel, UUIDv7 indexing, and Tombstone deletion flags, it replicates data seamlessly between local SQLite and remote edge nodes.
  4. H - HTMX: Bypasses complex JavaScript build chains. It fetches HTML fragments directly from our Robyn backend, simplifying client state management.

The Complete Architecture Handbook

If you want to build this system yourself, containing all 16 chapters of step-by-step guides, system startup logs, and packaging guides (compiling to a single 128MB .app / .exe bundle), check out our handbook:

📖 ERTH Assistant: Local-First + AI Sidecar Desktop Architecture on Leanpub (Includes a free 5-chapter preview edition!)

The full source code is completely open-sourced on GitHub:

👉 bnpysse/erth_assistant on GitHub

Thank you for following this series, and happy hacking!

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.