<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged htmx</title>
<link>https://coderlegion.com/tag/htmx</link>
<description>Powered by Question2Answer</description>
<item>
<title>htmx 4: What's New, What Breaks, Why It's Not latest</title>
<link>https://coderlegion.com/25745/htmx-4-whats-new-what-breaks-why-its-not-latest</link>
<description>&lt;p&gt;htmx 4.0.0 &lt;a rel=&quot;nofollow&quot; href=&quot;https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released&quot;&gt;shipped today, August 28, 2026&lt;/a&gt;. The ajax core now runs on &lt;code&gt;fetch()&lt;/code&gt; instead of &lt;code&gt;XMLHttpRequest&lt;/code&gt;, attribute inheritance is explicit, the back button issues a real request instead of restoring a cached snapshot, and error responses can be routed by status code. It is a major release that breaks real things, on purpose. And it comes with an unusual instruction: nobody has to move. On npm, 2.x keeps the &lt;code&gt;latest&lt;/code&gt; dist-tag until early 2027, 4.0 lives under &lt;code&gt;next&lt;/code&gt;, and htmx 2 stays supported indefinitely.&lt;/p&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Every request htmx makes now goes through &lt;code&gt;fetch()&lt;/code&gt;; &lt;code&gt;XMLHttpRequest&lt;/code&gt; is out, along with the XHR-specific events.&lt;/li&gt;
&lt;li&gt;Attribute inheritance is explicit: attributes like &lt;code&gt;hx-confirm&lt;/code&gt; reach descendants only when you add the &lt;code&gt;:inherited&lt;/code&gt; modifier.&lt;/li&gt;
&lt;li&gt;The history DOM cache is gone. The back button re-fetches the page from the server instead of restoring a stored snapshot.&lt;/li&gt;
&lt;li&gt;Error responses swap by default, and the new &lt;code&gt;hx-status&lt;/code&gt; attribute routes 4xx and 5xx responses by status code to their own targets.&lt;/li&gt;
&lt;li&gt;On npm, htmx 2.x keeps the &lt;code&gt;latest&lt;/code&gt; dist-tag until early 2027, and 4.0 lives under the &lt;code&gt;next&lt;/code&gt; dist-tag, so nobody upgrades by accident.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-changed-in-htmx-4&quot;&gt;What changed in htmx 4&lt;/h3&gt;
&lt;p&gt;htmx 4 is a ground-up rework of the library's internals and a cleanup of its accumulated defaults. Carson Gross, htmx's creator, announced it in an essay called &lt;a rel=&quot;nofollow&quot; href=&quot;https://htmx.org/essays/the-fetchening/&quot;&gt;The fetch()ening&lt;/a&gt; in November 2025, together with the first alpha. A string of betas followed through 2026, and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/bigskysoftware/htmx/releases&quot;&gt;final release landed today&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The big majors I covered this year, &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/astro-7&quot;&gt;Astro 7&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/pnpm-v12-rust-rewrite&quot;&gt;pnpm v12&lt;/a&gt;, rewrote their engines precisely so that nothing would change for you: same surface, faster internals. htmx 4 is the other kind of major. Moving to &lt;code&gt;fetch()&lt;/code&gt; gave the project a reason to revisit design decisions that had been locked in for years, and the team took it. Implicit inheritance, the history cache, silently ignored error responses: all gone. That is why the release strategy matters as much as the feature list, and I will get to it.&lt;/p&gt;
&lt;h3 id=&quot;the-fetch-rewrite&quot;&gt;The fetch() rewrite&lt;/h3&gt;
&lt;p&gt;Every request htmx makes now goes through &lt;code&gt;fetch()&lt;/code&gt; rather than &lt;code&gt;XMLHttpRequest&lt;/code&gt;. You do not call either API yourself when you write htmx, so on a quiet day you will not notice. The visible consequences are at the edges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;XHR-specific events are gone.&lt;/strong&gt; &lt;code&gt;htmx:xhr:loadstart&lt;/code&gt;, &lt;code&gt;htmx:xhr:progress&lt;/code&gt;, and &lt;code&gt;htmx:xhr:abort&lt;/code&gt; have no &lt;code&gt;fetch()&lt;/code&gt; equivalent and were removed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Events follow a new &lt;code&gt;htmx:phase:action&lt;/code&gt; naming scheme.&lt;/strong&gt; If you listen to htmx lifecycle events in JavaScript, every listener needs a rename.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A default request timeout exists now.&lt;/strong&gt; Requests time out after 60 seconds; in 2.x they could hang forever. The config option moved from &lt;code&gt;timeout&lt;/code&gt; to &lt;code&gt;defaultTimeout&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The event renames look like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;htmx 2.x                                                               &lt;/th&gt;&lt;th&gt;htmx 4&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;htmx:beforeRequest&lt;/code&gt;                                                   &lt;/td&gt;&lt;td&gt;&lt;code&gt;htmx:before:request&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;htmx:afterRequest&lt;/code&gt;                                                    &lt;/td&gt;&lt;td&gt;&lt;code&gt;htmx:after:request&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;htmx:beforeSwap&lt;/code&gt;                                                      &lt;/td&gt;&lt;td&gt;&lt;code&gt;htmx:before:swap&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;htmx:afterSwap&lt;/code&gt;                                                       &lt;/td&gt;&lt;td&gt;&lt;code&gt;htmx:after:swap&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;htmx:configRequest&lt;/code&gt;                                                   &lt;/td&gt;&lt;td&gt;&lt;code&gt;htmx:config:request&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;htmx:sendError&lt;/code&gt;, &lt;code&gt;htmx:swapError&lt;/code&gt;, &lt;code&gt;htmx:targetError&lt;/code&gt;, &lt;code&gt;htmx:timeout&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;htmx:error&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That last row is a real simplification: four error events collapse into one &lt;code&gt;htmx:error&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Extensions changed even more. The announcement puts it plainly: &quot;Switching to &lt;code&gt;fetch()&lt;/code&gt; internally let us rethink how extensions can and should work.&quot; The old &lt;code&gt;htmx.defineExtension()&lt;/code&gt; API is replaced by &lt;code&gt;htmx.registerExtension()&lt;/code&gt;, and the &lt;code&gt;hx-ext&lt;/code&gt; attribute is gone entirely: loading an extension's script is enough. There is a dedicated &lt;a rel=&quot;nofollow&quot; href=&quot;https://four.htmx.org/docs/extension-htmx-4-migration-guide&quot;&gt;extension migration guide&lt;/a&gt; for authors. The payoff is streaming: new first-party extensions stream HTML into the page over server-sent events (&lt;code&gt;hx-sse&lt;/code&gt;), WebSockets (&lt;code&gt;hx-ws&lt;/code&gt;), and &lt;code&gt;multipart/mixed&lt;/code&gt; responses (&lt;code&gt;hx-multipart&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;attribute-inheritance-is-now-explicit&quot;&gt;Attribute inheritance is now explicit&lt;/h3&gt;
&lt;p&gt;In htmx 2, attributes like &lt;code&gt;hx-confirm&lt;/code&gt; and &lt;code&gt;hx-target&lt;/code&gt; silently inherited from parent elements. Convenient until it wasn't: a &lt;code&gt;hx-confirm&lt;/code&gt; on a container would attach confirmation dialogs to descendants you forgot about, and &lt;code&gt;hx-disinherit&lt;/code&gt; existed purely to fight this. In htmx 4, inheritance only happens when you ask for it with the &lt;code&gt;:inherited&lt;/code&gt; modifier:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div hx-confirm:inherited=&quot;Are you sure?&quot;&amp;gt;
  &amp;lt;button hx-delete=&quot;/account&quot;&amp;gt;Delete my account&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the modifier, the attribute applies only to the element it sits on. &lt;code&gt;hx-inherit&lt;/code&gt; and &lt;code&gt;hx-disinherit&lt;/code&gt; are both removed, since explicit inheritance makes them pointless. If your codebase leans heavily on the old behavior, setting &lt;code&gt;implicitInheritance&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in the htmx config restores it while you migrate.&lt;/p&gt;
&lt;p&gt;Of everything in this release, this is the change I would have asked for. Implicit inheritance was the closest thing htmx had to action at a distance, and debugging it meant walking up the DOM tree in your head.&lt;/p&gt;
&lt;h3 id=&quot;the-back-button-is-a-real-request-now&quot;&gt;The back button is a real request now&lt;/h3&gt;
&lt;p&gt;htmx 2 handled history by snapshotting the DOM into browser storage (localStorage originally, sessionStorage &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/bigskysoftware/htmx/releases/tag/v2.0.5&quot;&gt;since 2.0.5&lt;/a&gt;) and restoring the snapshot on back navigation. That cache is exactly the kind of client-side state that drifts from reality, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.infoworld.com/article/4150864/htmx-4-0-hypermedia-finds-a-new-gear.html&quot;&gt;InfoWorld's coverage&lt;/a&gt; of the 4.0 line put it bluntly: &quot;It turned out to be brittle.&quot;&lt;/p&gt;
&lt;p&gt;htmx 4 drops the cache entirely. Per the &lt;a rel=&quot;nofollow&quot; href=&quot;https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released&quot;&gt;announcement&lt;/a&gt;, on back navigation htmx re-fetches the page from the server and swaps it into &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. Your back button now shows what the server says the page is, not what the page happened to look like when you left it. The &lt;code&gt;hx-history&lt;/code&gt; attribute and &lt;code&gt;historyCacheSize&lt;/code&gt; config are gone with it. If you genuinely need snapshot behavior, an &lt;code&gt;hx-history-cache&lt;/code&gt; extension brings it back as an opt-in.&lt;/p&gt;
&lt;h3 id=&quot;route-error-responses-with-hx-status&quot;&gt;Route error responses with hx-status&lt;/h3&gt;
&lt;p&gt;htmx 2 refused to swap 400 and 500 responses by default, which meant server-rendered validation errors needed workarounds. htmx 4 flips the default: 4xx and 5xx responses swap like any other (only 204 and 304 are no-swap by default), and a new &lt;code&gt;hx-status&lt;/code&gt; attribute routes specific status codes to their own target and swap behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;form
  hx-post=&quot;/signup&quot;
  hx-status:422=&quot;target:#validation-errors&quot;
  hx-status:5xx=&quot;target:#server-error&quot;
  hx-status:503=&quot;swap:none&quot;
&amp;gt;&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Matching goes from specific to general: an exact code like &lt;code&gt;503&lt;/code&gt; wins over a single-digit wildcard like &lt;code&gt;50x&lt;/code&gt;, which wins over a range wildcard like &lt;code&gt;5xx&lt;/code&gt;. A 422 with the form's field errors rendered server-side can now land in the right spot with one attribute, which is what hypermedia apps do all day. If you depended on the old ignore-errors behavior, setting the &lt;code&gt;noSwap&lt;/code&gt; config to &lt;code&gt;[204, 304, '4xx', '5xx']&lt;/code&gt; restores it.&lt;/p&gt;
&lt;h3 id=&quot;what-else-is-new-in-htmx-4&quot;&gt;What else is new in htmx 4&lt;/h3&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://four.htmx.org/docs/whats-new-in-htmx-4&quot;&gt;what's new page&lt;/a&gt; lists more than I can cover, but a few stand out. Morph swaps are built in: &lt;code&gt;innerMorph&lt;/code&gt; and &lt;code&gt;outerMorph&lt;/code&gt; use the idiomorph algorithm and need no extension. New swap styles &lt;code&gt;textContent&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; join the existing set. A new &lt;code&gt;&amp;lt;hx-partial&amp;gt;&lt;/code&gt; element lets one response update several targets, with each partial carrying its own target and swap. View Transitions support is there but disabled by default; flip the &lt;code&gt;transitions&lt;/code&gt; config option to turn it on.&lt;/p&gt;
&lt;p&gt;The one that caught my eye: a new &lt;code&gt;hx-query&lt;/code&gt; attribute issues an HTTP QUERY request, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/the-new-http-query-method&quot;&gt;safe-reads-with-a-body method&lt;/a&gt; I wrote about in July after RFC 10008 landed. Watching a brand-new HTTP verb show up as a first-class attribute in a hypermedia library within months is the spec pipeline working the way it should.&lt;/p&gt;
&lt;h3 id=&quot;the-breaking-changes-in-htmx-4&quot;&gt;The breaking changes in htmx 4&lt;/h3&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://four.htmx.org/docs&quot;&gt;migration guide&lt;/a&gt; is thorough, and the headline breaking changes are the three above: explicit inheritance, error responses swapping by default, and server round-trip history. Beyond those:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Two attributes trade names, and order matters.&lt;/strong&gt; &lt;code&gt;hx-disable&lt;/code&gt; becomes &lt;code&gt;hx-ignore&lt;/code&gt;, and &lt;code&gt;hx-disabled-elt&lt;/code&gt; becomes &lt;code&gt;hx-disable&lt;/code&gt;. The old name gets reused, so the guide says to rename &lt;code&gt;hx-disable&lt;/code&gt; to &lt;code&gt;hx-ignore&lt;/code&gt; first or you will migrate one attribute into the other.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Removed attributes:&lt;/strong&gt; &lt;code&gt;hx-ext&lt;/code&gt;, &lt;code&gt;hx-inherit&lt;/code&gt;, &lt;code&gt;hx-disinherit&lt;/code&gt;, &lt;code&gt;hx-history&lt;/code&gt;, &lt;code&gt;hx-vars&lt;/code&gt; (use &lt;code&gt;hx-vals&lt;/code&gt; with the &lt;code&gt;js:&lt;/code&gt; prefix), &lt;code&gt;hx-params&lt;/code&gt; (use the &lt;code&gt;htmx:config:request&lt;/code&gt; event), &lt;code&gt;hx-request&lt;/code&gt;, and &lt;code&gt;hx-prompt&lt;/code&gt; (now an extension).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Config options were renamed or culled.&lt;/strong&gt; &lt;code&gt;timeout&lt;/code&gt; is &lt;code&gt;defaultTimeout&lt;/code&gt;, &lt;code&gt;defaultSwapStyle&lt;/code&gt; is &lt;code&gt;defaultSwap&lt;/code&gt;, &lt;code&gt;globalViewTransitions&lt;/code&gt; is &lt;code&gt;transitions&lt;/code&gt;. A long list of options is simply gone, including &lt;code&gt;historyCacheSize&lt;/code&gt;, &lt;code&gt;defaultSwapDelay&lt;/code&gt;, and the settling-class options.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation events are removed.&lt;/strong&gt; &lt;code&gt;htmx:validation:validate&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, and &lt;code&gt;halted&lt;/code&gt; are out; native browser validation is the path forward.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every event listener needs the new names.&lt;/strong&gt; See the table above.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;htmx ships a scanner that finds most of this for you:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npx htmx.org@4.0.0 upgrade-check -- ./path/to/project/root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It walks your templates and prints deprecated patterns with file and line numbers. Run it before deciding how big your migration actually is; for a small app the answer may be an afternoon.&lt;/p&gt;
&lt;h3 id=&quot;why-4-0-is-not-latest-on-npm&quot;&gt;Why 4.0 is not latest on npm&lt;/h3&gt;
&lt;p&gt;Here is the part I find genuinely rare. As of today, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/htmx.org&quot;&gt;htmx.org package&lt;/a&gt; has &lt;code&gt;latest&lt;/code&gt; pointing at 2.0.10 and 4.0.0 published under &lt;code&gt;next&lt;/code&gt;. That is deliberate, and the announcement spells out why:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;we are not marking 4.0 as &lt;code&gt;latest&lt;/code&gt; in NPM because we do not want to force-upgrade users who are relying on non-versioned CDN URLs for htmx. Instead, 2.x will remain &lt;code&gt;latest&lt;/code&gt; and the 4.0 line will remain &lt;code&gt;next&lt;/code&gt; until some point in early 2027.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Plenty of production sites load htmx from a CDN URL that resolves whatever &lt;code&gt;latest&lt;/code&gt; is. Flipping the tag on release day would silently ship breaking changes to all of them. So the tag waits several more months while 4.0 is, by every other measure, released and stable today.&lt;/p&gt;
&lt;p&gt;The announcement frames the whole release this way: &quot;we have made explicit choices that we feel will put htmx-based applications in a good spot for being 100-year web services.&quot; You can roll your eyes at the century talk, but the mechanics back it up. htmx 2 &quot;will continue to be supported indefinitely so don't feel any pressure to upgrade,&quot; per the same announcement. Compare that with the upgrade treadmill most of the ecosystem runs on, where staying one major behind makes you a straggler within months. htmx is telling you the old version is fine, forever, and meaning it.&lt;/p&gt;
&lt;h3 id=&quot;should-you-upgrade-to-htmx-4-&quot;&gt;Should you upgrade to htmx 4?&lt;/h3&gt;
&lt;p&gt;For a new project, start on 4. The defaults are better: explicit inheritance, error responses you can actually handle in hypermedia, history that cannot go stale. Install it with &lt;code&gt;npm install htmx.org@next&lt;/code&gt; or pin the 4.0.0 CDN URL, since &lt;code&gt;latest&lt;/code&gt; will hand you 2.x until early 2027.&lt;/p&gt;
&lt;p&gt;For an existing app, there is no clock. Run the &lt;code&gt;upgrade-check&lt;/code&gt; scanner to size the work, and if the report is short, do the migration in an isolated &lt;a rel=&quot;nofollow&quot; href=&quot;https://morello.dev/blog/git-worktrees-are-underrated&quot;&gt;git worktree&lt;/a&gt; and shake out the event renames. If the report is long, htmx 2 remains supported and maintained, and the team has said so in writing.&lt;/p&gt;
&lt;p&gt;The technical changes are good. The release discipline is the story I will remember: a project that shipped a breaking major and then deliberately kept it off the &lt;code&gt;latest&lt;/code&gt; tag so nobody gets upgraded by accident. That is what taking backwards compatibility seriously looks like.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25745/htmx-4-whats-new-what-breaks-why-its-not-latest</guid>
<pubDate>Sat, 29 Aug 2026 07:27:27 +0000</pubDate>
</item>
<item>
<title>Testing HTMX apps in the real browser with TWD</title>
<link>https://coderlegion.com/23392/testing-htmx-apps-in-the-real-browser-with-twd</link>
<description>&lt;p&gt;HTMX apps are refreshingly simple: the server returns HTML fragments, and HTMX swaps them into the page in response to &lt;code&gt;hx-get&lt;/code&gt;, &lt;code&gt;hx-post&lt;/code&gt;, and friends. There is usually no bundler and no client framework. That simplicity is the point, and it is also why the usual testing tools feel wrong. A jsdom unit test cannot see an HTMX swap, and a full end-to-end runner is a lot of machinery to bolt onto an app that deliberately avoids machinery.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&gt;twd-js&lt;/a&gt; fits the HTMX model well. It runs tests inside the real browser, so it sees exactly what HTMX does to the DOM, and it installs from a CDN with an import map, so it adds no build step of its own.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/bk54c3v9flr200opkz9h.png&quot; alt=&quot;TWD sidebar running tests inside an HTMX app&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;setup-load-both-from-a-cdn&quot;&gt;Setup: load both from a CDN&lt;/h3&gt;
&lt;p&gt;Load HTMX as usual, then add an &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap&quot;&gt;import map&lt;/a&gt; for TWD next to it. &lt;a rel=&quot;nofollow&quot; href=&quot;https://esm.sh&quot;&gt;esm.sh&lt;/a&gt; resolves TWD's internal dependency, so there is nothing else to wire up.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script src=&quot;https://unpkg.com/htmx.org@2.0.10/dist/htmx.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type=&quot;importmap&quot;&amp;gt;
  {
    &quot;imports&quot;: {
      &quot;twd-js&quot;: &quot;https://esm.sh/twd-js@1.8.2&quot;,
      &quot;twd-js/runner&quot;: &quot;https://esm.sh/twd-js@1.8.2/runner&quot;,
      &quot;twd-js/bundled&quot;: &quot;https://esm.sh/twd-js@1.8.2/bundled&quot;
    }
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your markup stays pure HTMX:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;button hx-get=&quot;/api/todos&quot; hx-target=&quot;#todo-list&quot; hx-swap=&quot;innerHTML&quot;&amp;gt;Load todos&amp;lt;/button&amp;gt;
&amp;lt;ul id=&quot;todo-list&quot;&amp;gt;&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;test-against-the-real-backend&quot;&gt;Test against the real backend&lt;/h3&gt;
&lt;p&gt;The important design choice for HTMX is what to assert against. Because HTMX endpoints return HTML fragments rather than JSON, the cleanest approach is to run the tests against your real HTML-returning backend and reset it between tests, rather than mocking. This is the same pattern the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-nuxt-example&quot;&gt;Nuxt example&lt;/a&gt; uses for real-backend testing.&lt;/p&gt;
&lt;p&gt;A tiny dev-only reset endpoint gives every test a clean starting point:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { twd, userEvent, screenDom } from 'twd-js';
import { describe, it, beforeEach } from 'twd-js/runner';

describe('Todo List (HTMX)', () =&amp;gt; {
  beforeEach(async () =&amp;gt; {
    await fetch('/api/reset', { method: 'POST' });
  });

  it('loads the seeded todos', async () =&amp;gt; {
    await twd.visit('/');

    const loadButton = await screenDom.getByRole('button', { name: 'Load todos' });
    await userEvent.click(loadButton);

    // HTMX swaps asynchronously, so use findBy* (which waits) rather than getBy*.
    const todo = await screenDom.findByText('Learn TWD');
    twd.should(todo, 'be.visible');
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The one detail worth internalizing: HTMX swaps happen after an async request, and there is no mock to await, so query with Testing Library's &lt;code&gt;findBy*&lt;/code&gt; (which retries until the element appears) instead of the synchronous &lt;code&gt;getBy*&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;a-note-on-mocking&quot;&gt;A note on mocking&lt;/h3&gt;
&lt;p&gt;TWD's service worker will happily intercept HTMX's requests, the same as any &lt;code&gt;fetch&lt;/code&gt; or XHR. Today &lt;code&gt;twd.mockRequest&lt;/code&gt; serializes responses as JSON, which is a natural fit for JSON APIs but not for HTML fragments, so for HTMX we recommend the real-backend approach above. First-class HTML-fragment mocking for hypermedia frameworks is on the roadmap.&lt;/p&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;A complete example, an HTMX todo list with load, create, and delete, a small Express backend with a reset endpoint, in-browser tests, and CI, lives in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-htmx&quot;&gt;twd-htmx&lt;/a&gt;. It is served as static files with TWD loaded straight from the CDN, so you can read the whole thing top to bottom without a build step in sight.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23392/testing-htmx-apps-in-the-real-browser-with-twd</guid>
<pubDate>Fri, 24 Jul 2026 09:04:43 +0000</pubDate>
</item>
<item>
<title>Agentic AI: Good Upfront Design Pays You Back Later</title>
<link>https://coderlegion.com/22078/agentic-ai-good-upfront-design-pays-you-back-later</link>
<description>&lt;p&gt;I spend a lot of time preaching architecture and constraints, so it is always nice when a side project gives me receipts. Adding this new feature to DumbQuestion.ai was a good reminder that a well-structured first version lets you spend your next iteration on value, not repair.&lt;/p&gt;
&lt;p&gt;Below, you will find a few relatively simple challenges and how thoughtful, upfront design made the changes effortless.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To vibe or not to vibe ...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many developers jump right in and just rip out an app, ship fast, let the coding agent sort it out, come back and deal with it later. To be fair, that absolutely can get you to first release faster. But even on a solo project, a little proper SDLC discipline pays back later when you want to extend the product without turning every feature into a rescue mission, which is a theme that already runs through how I have been building DumbQuestion.ai.&lt;/p&gt;
&lt;p&gt;Extend this to the enterprise and you turn a little upfront effort into potential huge savings on token spend&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Roasting starup pitches (for sport) ...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The core idea for Startup Roast was simple enough: take a startup pitch, roast it, and add a reality-check section so the output is not just mockery for mockery’s sake. To illustrate (and avoid just vaguely describing the feature) I picked a random but highly upvoted pitch from Product Hunt: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.producthunt.com/products/vida-5&quot;&gt;Vida&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Vida, which pitches itself as an “AI clone” that learns how you work, remembers what matters, and becomes a “second you,” with early use cases like Reply Rescue, Prompt Rescue, Resume Rescue, Workspace Cleanup, and Daily Wrap. This is a pretty common target use case of agentic AI making it a solid candidate.&lt;/p&gt;
&lt;p&gt;If you want to skip ahead, here's &lt;a rel=&quot;nofollow&quot; href=&quot;https://dumbquestion.ai/q/roast-my-startup-7XHTxqyD?utm_source=roastlegion&quot;&gt;an example roast&lt;/a&gt; for Vida.&lt;/p&gt;
&lt;p&gt;Combining a preliminary web &quot;market search&quot; into the content yielded a result that was not just sarcastic, but informed. The roast hit the obvious AI-clone positioning, questioned whether the product was really a clone versus a macro suite, and then turned the market context into a sharper Reality Check about integration bugs, weak retention, and the risk of becoming yet another chat-wrapper-style productivity startup.&lt;/p&gt;
&lt;h3 id=&quot;challenge-1-prompt-size-parameterization&quot;&gt;Challenge 1: Prompt Size / Parameterization&lt;/h3&gt;
&lt;p&gt;DumbQuestion.ai originally dealt with a short question (&amp;lt; 100 characters). The startup roast feature deals with a whole pitch, which is a very different shape of input, with more context, more structure, and more ways for the prompt to get noisy. The Vida example alone is long enough to make that obvious.&lt;/p&gt;
&lt;p&gt;This is where boring code quality decisions start paying rent, something likely caught in a code review: use configuration or constants insead of hardcoded literals. If prompt limits, prompt assembly, and related values are already treated like configurable extension points instead of scattered magic numbers, adapting to longer input becomes an iteration problem instead of a rewrite problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This was an instant code and unit test change&lt;/strong&gt;. This led to an even better refactor to make the limit parametric instead of constant.&lt;/p&gt;
&lt;h3 id=&quot;challenge-2-search-phrases-and-market-context&quot;&gt;Challenge 2: Search Phrases and Market Context&lt;/h3&gt;
&lt;p&gt;A generic roast is funny once. A roast that feels aware of the market is a better product artifact. In the Vida example, adding in a quick prelminary web search using search phrases like “top competitors to AI work clone or AI agent that learns your habits startup” and “reasons AI personal assistant or productivity agent startups fail,” were added to the context to shape the final output. It moved the result to something more specific than just &quot;ha ha, another AI startup.”&lt;/p&gt;
&lt;p&gt;To support that, I added a preliminary LLM pass to extract search phrases as a JSON array, then routed those through &lt;em&gt;my existing multi-provider web search capability&lt;/em&gt; and used the results to prefill the final LLM prompt. The point here is not that this was especially hard. The point is that it was not hard because search was already isolated as a reusable feature instead of buried inside one specific interaction path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The pay back was immediate&lt;/strong&gt;. If search is a first-class capability, a new feature can borrow it immediately. The architectural work already happened earlier. The new work is mostly deciding how to apply it.&lt;/p&gt;
&lt;h3 id=&quot;challenge-3-markdown-output&quot;&gt;Challenge 3: Markdown Output&lt;/h3&gt;
&lt;p&gt;Once the responses got richer, plain text stopped being enough. Roast plus Reality Check wants structure. The Vida output is a good example because it has a clear voice shift from mockery into analysis, and that reads much better when the response can preserve formatting cleanly. Different models chose different formats and whether or not to pepper in emojis.&lt;/p&gt;
&lt;p&gt;Again, this is where earlier organization matters more than raw cleverness. If your HTMX partials and rendering boundaries are reasonably clean, adding markdown support is much lower impact than it would be in a tangled UI layer. Pull in a Go lib dependency, apply it in a single, small partial, update a focused unit test and very few tokens were needed to wrap up this feature.&lt;/p&gt;
&lt;p&gt;Quiet structural decisions like that are what let a new feature stay contained.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A key lesson here for sure&lt;/strong&gt;: nearly all first runs with a coding agent generate &quot;balls of mud&quot; in the UI instead of well organized components. Nip this one in your first code review if not sooner.&lt;/p&gt;
&lt;h3 id=&quot;challenge-4-prompt-injection-tuning&quot;&gt;Challenge 4: Prompt Injection Tuning&lt;/h3&gt;
&lt;p&gt;The moment you let people paste startup pitches into an LLM tool, you are no longer just handling simple, earnest user input. You are also handling jokes, garbage, hostile instructions, and people trying to be clever because of course they are. That was already true in DumbQuestion.ai, but longer, richer pitch inputs make it even more relevant.&lt;/p&gt;
&lt;p&gt;I already had prompt injection handling in place, but Startup Roast needed the tuning adjusted. Because the detection and response behavior were already parameterized instead of hard-coded into a pile of special cases, tuning it became a smaller, more contained change. Better thresholds, better handling, smaller git diff, fewer tokens.&lt;/p&gt;
&lt;h3 id=&quot;challenge-5-model-and-persona-tuning&quot;&gt;Challenge 5: Model and Persona Tuning&lt;/h3&gt;
&lt;p&gt;Startup pitches are a different input class than dumb questions, so I wanted to re-test model speed, quality, and instruction adherence under longer prompts and a different tone. Luckily, I had already built an eval harness for comparing models, so adding startup-pitch cases was an easy extension instead of a new side quest. This was already incorporated into my unit, e2e and integration tests so &lt;strong&gt;even for a solo project&lt;/strong&gt;, this paid off.&lt;/p&gt;
&lt;p&gt;The persona side worked out the same way. Because the personas were already packaged to be &lt;strong&gt;open for extension&lt;/strong&gt;, adding new instructions and persona-specific CTAs was low impact. That let me spend time tuning tone and usefulness instead of untangling brittle prompt logic.&lt;/p&gt;
&lt;p&gt;For the curious, here's the same pitch roasted across the four personas: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dumbquestion.ai/q/roast-my-startup-7XHTxqyD?utm_source=roastlegion&quot;&gt;Weary&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://dumbquestion.ai/q/roast-my-startup-1HLevhkf?utm_source=roastlegion&quot;&gt;Overqualified&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://dumbquestion.ai/q/roast-my-startup-MhGjmxvh?utm_source=roastlegion&quot;&gt;Compliant&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://dumbquestion.ai/q/roast-my-startup-wQvl2qK4?utm_source=roastlegion&quot;&gt;\[REDACTED\]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I always like the insane positivity The Compliant brings to the table.&lt;/p&gt;
&lt;h3 id=&quot;the-payback&quot;&gt;The Payback&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Focus on value add&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Good upfront design changes what kind of work you get to do next. It lets you spend your energy on product judgment instead of repair work. It turns feature additions into focused passes on value: better prompts, better search context, better output formatting, better guardrails.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Design efficiency = Token efficiency&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It also makes coding agents more useful and more efficient. AI coding agents are great at implementation, but they are much more effective when the system already has clean extension points, isolated responsibilities, and tunable behavior. A prompt-length constant is an extension point. Multi-provider search reuse is an extension point. Modular rendering is an extension point. Parametric prompt-injection tuning is an extension point. Those are not giant innovations. They are just design decisions that compound later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Team of One / Team of Many&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That is why I still believe even solo projects deserve at least a little SDLC respect. Not enterprise ceremony. Not fifty approval gates. Just enough thought, structure, and code quality that future-you is allowed to work on the interesting part.&lt;/p&gt;
&lt;p&gt;If it pays off a little for a team of one, it can pay off a lot for a team of many.&lt;/p&gt;
&lt;p&gt;For this feature, the interesting part was not fighting the code. It was thinking about how to make the roast more specific, more grounded, and more shareable. The Vida example was a nice reminder that good architecture really does buy that freedom later.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22078/agentic-ai-good-upfront-design-pays-you-back-later</guid>
<pubDate>Wed, 08 Jul 2026 00:00:41 +0000</pubDate>
</item>
</channel>
</rss>