← All guides

A mock backend for htmx — endpoints that speak HTML, not JSON

htmx has a mocking problem nobody talks about: htmx swaps HTML, but every mock API tool speaks JSON. mockapi.io, JSONPlaceholder, json-server, MSW handlers in most tutorials — all JSON. Point hx-get at any of them and you'll swap a JSON blob into your page as text. To prototype an htmx UI before the backend exists, you need endpoints that return HTML fragments — ideally with real network latency, real error statuses, and server-side rendering of what you submitted.

Mockbird's custom routes do exactly that: define a path, give it an HTML template with {{query.x}} / {{body.x}} placeholders, and you have a hosted URL that answers htmx with rendered fragments. Form posts (urlencoded and multipart — htmx's defaults) are parsed into {{body.field}}. Free, no signup. Every snippet below was run in a real browser before publishing (htmx 2.0.7).

1. Get a fragment endpoint (30 seconds, no signup)

curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
  -H 'content-type: application/json' -d '{"name":"htmx-demo"}'
# → {"id":"abc123","adminKey":"...","baseUrl":"https://mockbird.mockbird.workers.dev/m/abc123", ...}

curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123/routes \
  -H 'x-admin-key: YOUR_ADMIN_KEY' -H 'content-type: application/json' -d '{
    "method":"GET", "path":"/fragments/greeting",
    "contentType":"text/html; charset=utf-8",
    "body":"<p>Hello, <b>{{query.name}}</b>! Rendered server-side at {{now}}.</p>"
  }'

curl "https://mockbird.mockbird.workers.dev/m/abc123/fragments/greeting?name=Ada"
# → <p>Hello, <b>Ada</b>! Rendered server-side at 2026-08-03T00:48:49.400Z.</p>

Prefer a UI? One click creates a project in the dashboard; the Custom routes card does the same thing as the curl.

2. The htmx 2.0 gotcha that will hit you first: selfRequestsOnly

Wire that endpoint into a page and — in htmx 2.x — nothing happens, with htmx:invalidPath in the console. htmx 2.0 changed htmx.config.selfRequestsOnly to default true: requests to any other origin are refused unless you opt in. A mock backend is by definition another origin, so:

<meta name="htmx-config" content='{"selfRequestsOnly":false}'>
<script src="https://unpkg.com/htmx.org@2.0.7"></script>

<button hx-get="https://mockbird.mockbird.workers.dev/m/abc123/fragments/greeting?name=Ada"
        hx-target="#greeting" hx-swap="innerHTML">Load greeting</button>
<div id="greeting"></div>

That's the whole fix (htmx 1.x doesn't need it). Mockbird's side is already handled: CORS is open, and htmx's HX-Request / HX-Target / HX-Trigger request headers are in our Access-Control-Allow-Headers — a detail that silently breaks cross-origin htmx against many servers, because htmx sends those headers on every request and a failed preflight looks like "the button just doesn't work".

3. Forms: hx-post rendered server-side

htmx posts forms as application/x-www-form-urlencoded (or multipart). Mockbird parses both into {{body.field}}, so the success fragment can echo what the user typed — like a real server would:

curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123/routes \
  -H 'x-admin-key: YOUR_ADMIN_KEY' -H 'content-type: application/json' -d '{
    "method":"POST", "path":"/subscribe",
    "contentType":"text/html; charset=utf-8",
    "body":"<div id=\"signup\" class=\"ok\">✓ Thanks, <b>{{body.email}}</b> — you are on the list.</div>"
  }'
<form hx-post="https://mockbird.mockbird.workers.dev/m/abc123/subscribe"
      hx-target="#signup" hx-swap="outerHTML">
  <input type="email" name="email" placeholder="you@example.com" required>
  <button type="submit">Subscribe</button>
</form>
<div id="signup"></div>

Submit → the fragment comes back with the address rendered in. Every hit (with headers and body) also lands in the project's request inspector, so you can see exactly what htmx sent.

4. Loading states: spinners against real latency

Append ?mock_delay=1500 (ms) to any URL — or set delayMs on the route — and your hx-indicator finally has something to indicate:

<style>
  .htmx-indicator{display:none}
  .htmx-request.htmx-indicator, .htmx-request .htmx-indicator{display:inline}
</style>

<button hx-get="https://mockbird.mockbird.workers.dev/m/abc123/fragments/greeting?name=Slow&mock_delay=1500"
        hx-target="#slow" hx-indicator="#spin">Load slow</button>
<span id="spin" class="htmx-indicator">⏳ loading…</span>

Second gotcha, verified the hard way: when hx-indicator points at an element outside the trigger, htmx puts the htmx-request class on the indicator itself — so a CSS rule with only the descendant form (.htmx-request .htmx-indicator) never matches. Include the combined selector above (htmx's auto-injected default styles use opacity and handle both; the moment you write your own display-based rule you need both too).

5. Error states: what does your UI do on a 500?

htmx does not swap error responses by default — it fires htmx:responseError and leaves the page untouched, which in an unhandled app means the button silently does nothing. Force real errors with ?mock_status and make sure somebody is listening:

<button hx-get="https://mockbird.mockbird.workers.dev/m/abc123/fragments/greeting?mock_status=500"
        hx-target="#zone">Load</button>

<script>
document.body.addEventListener('htmx:responseError', (e) => {
  document.getElementById('zone').textContent =
    'Something broke (' + e.detail.xhr.status + ') — retry?';
});
</script>

Sweep the whole matrix — mock_status=401, 404, 429, 503 — by editing a query param, no server restarts. Add ?mock_chaos=0.3 to fail a random 30% of requests and watch how your UI behaves when the network is having a day. More recipes: loading & error states.

6. Active search — the classic htmx demo, no backend

curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123/routes \
  -H 'x-admin-key: YOUR_ADMIN_KEY' -H 'content-type: application/json' -d '{
    "method":"GET", "path":"/search",
    "contentType":"text/html; charset=utf-8",
    "body":"<li>Result for <i>{{query.q}}</i> #1</li><li>Result for <i>{{query.q}}</i> #2</li>"
  }'
<input type="text" name="q"
       hx-get="https://mockbird.mockbird.workers.dev/m/abc123/search"
       hx-trigger="keyup changed delay:200ms" hx-target="#results" hx-swap="innerHTML">
<ul id="results"></ul>

Type → debounced request → fragment echoes the query back. Add mock_delay to rehearse what fast typers see on slow networks.

7. Server-driven events: HX-Trigger response headers

Routes can set response headers (templated, too). Return HX-Trigger and htmx fires a DOM event you can hook anywhere on the page — the standard pattern for "the server says refresh that other widget":

# add "headers" to the /subscribe route from §3:
  "headers": {"HX-Trigger": "subscribed"}
<script>
document.body.addEventListener('subscribed', () => {
  console.log('server said: subscribed');   // refresh a counter, show a toast…
});
</script>

Detail that matters cross-origin: browsers hide response headers from XHR unless the server lists them in Access-Control-Expose-Headers. Mockbird exposes HX-Trigger, HX-Redirect, HX-Reswap, HX-Retarget, HX-Location and friends — set them on a route and htmx actually sees them. (We verified the event fires in a real browser; a server that emits the header but doesn't expose it fails silently.)

8. Host the whole page on Mockbird

The page itself can be a custom route: contentType: "text/html", paste a full document, and share a live htmx prototype as a URL — no hosting setup at all. Relative hx-get="fragments/greeting" resolves against the project base, so the page and its endpoints travel together:

curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123/routes \
  -H 'x-admin-key: YOUR_ADMIN_KEY' -H 'content-type: application/json' \
  -d "$(python3 -c 'import json;print(json.dumps({
    "method":"GET","path":"/page",
    "contentType":"text/html; charset=utf-8",
    "body":open("page.html").read()}))')"
# → https://mockbird.mockbird.workers.dev/m/abc123/page is a live htmx app

Honest fine print: these pages are served with a strict CSP sandbox (scripts run, forms and htmx requests work — we verified the full hx-get + hx-post loop in a browser — but cookies and localStorage are unreachable by design, so one project's page can never touch another visitor's Mockbird session). Great for prototypes and demos; not a general web host. Templates are capped at 16 KB, so load htmx from a CDN.

9. What about fragments backed by real records?

Being honest about the boundary: route templates interpolate request values ({{query.x}}, {{body.x}}, {{params.x}}, {{now}}, {{uuid}}…) — they don't loop over your project's stored records. If you need a list of 30 seeded products as <li>s, either: keep the fragment representative rather than data-driven (fine for most prototypes); fetch the JSON resource endpoints from a small inline script and render client-side; or use htmx's client-side-templates extension, which was built for exactly this — JSON API in, rendered template out. The JSON side of Mockbird (full CRUD, filtering, relations, pagination) works unchanged alongside your HTML routes.

10. Honest comparison

Hand-written dev server (Flask/Express)Static HTML fixture filesMockbird custom routes
Returns HTML fragments✔ anything✔ templated
Renders submitted form data✗ static{{body.field}}
Loops / conditionals in templates✔ real template engine✗ interpolation only (§9)
Latency / error / chaos simulationYou write itmock_delay / mock_status / mock_chaos
SetupProject + code + run a processFiles + a static serverTwo curls, hosted
Shareable URL (teammates, phone, CodePen)Only with tunnelingOnly if deployed✔ it's already a URL
Works offline
See what htmx actually sentYou add logging✔ request inspector

If your prototype needs data-driven loops server-side, a tiny Flask app is genuinely the better tool. For everything before that point — trying htmx ideas, demos, tutorials, testing loading/error UX, sharing a clickable prototype — a hosted fragment endpoint is two curls.

Data here is fake and seeded (no LLM involved); requests are capped daily per project. Create your own project — free, no signup: one click or one curl (§1). Related: custom endpoints · loading & error states · request bins · SSE streams (pairs with htmx's sse extension).