DEV Community

Cover image for Why I proxy the app instead of importing the components
Aejkatappaja
Aejkatappaja

Posted on

Why I proxy the app instead of importing the components

I was building a workout tracker in Go, templ and htmx, and I wanted the one thing every frontend dev takes for granted: a place to look at a single component on its own, poke at it, and see what it does. Storybook. But every time I reached for the server-rendered equivalent, the model felt wrong, and it took me a while to articulate why.

The import model

Storybook, and almost everything like it, works by importing your component into an isolated runtime. You hand it a component and some props, it renders it in a sandbox. This is a great fit for React, because a React component really is a pure-ish function of its props. The component is the unit. Render it with inputs, you get the output. Nothing else is needed.

Hypermedia breaks that assumption. In an htmx app, the interesting part of a component isn't the initial HTML, it's what happens when you interact with it: a button fires hx-post, the server responds with a fragment, htmx swaps it into a target, maybe an out-of-band swap updates something else, maybe a response header redirects. The behavior doesn't live in the component. It lives in the server: the routes, the handlers, the swap semantics, the headers.

So if you import an htmx fragment into an isolated JS harness, you get a dead shell. The markup renders, but every interaction hits a network layer that isn't there. To make it live again, you have to stand up a fake one: mock every route the component might call, in JavaScript, in the harness. You end up reimplementing a slice of your backend in a second language just to preview a button. That's backwards. The whole reason you picked htmx was to not do that.

The thing that already works

Here's the part that felt obvious once I saw it. I already had something that renders the component correctly and responds to its requests correctly: my running app. It knows the routes. It produces the real fragments. It sets the real headers. Why would I reimport the component into a worse environment when the correct environment is already running on :8080?

So instead of importing, I sit in front. A reverse proxy. The workbench runs as a transparent proxy ahead of your app and serves its own UI under /__sb/.
Everything else passes through untouched. Because the preview and the app share an origin, an htmx request fired from a preview reaches your real handler with no CORS, no URL rewriting, no mock server to boot. The fragment that comes back is the one your app actually produces.

That reframes what a "mock" is, too. In the import model, mocking is mandatory: nothing works without it. In the proxy model, mocking is opt-in. By default requests hit the real app. You mock a specific route only when you want to (no auth, no database), and everything else stays real. You can even run against a throwaway dev database and exercise the real thing end to end. And when you do mock, a mock can now declare a non-2xx status, so you can preview a component's failure swap (hx-target-error, status-conditional swaps) with the same proxy that serves its happy path. For components that sit behind auth, a --header flag injects a session cookie (or any header) into every forwarded request, so protected previews render in safe and live mode without a fake login.

And because every request physically passes through the proxy, the workbench can observe the actual lifecycle: the request, its params, the element it swapped into (highlighted in the preview), the status, the timing, the HTML that came back. Not a reconstruction. The real thing.

What it costs

This isn't free, and I don't want to oversell it.

  • It needs your app running. There's no static build artifact you can render offline. That's the deal: the app is the source of truth, so the app has to be up. You trade Storybook's zero-dependency isolation for fidelity, though opt-in mocking lets you detach a route from the DB or auth when you want that isolation back.
  • It is strictly a local dev tool. To frame previews it strips X-Frame-Options and CSP from proxied responses, and it proxies your whole app. You never run this in production or expose it publicly.
  • htmx is the path I've tested deepest. Turbo, Unpoly and Datastar now have probes verified end to end against the real libraries in a browser, but htmx is still where the tool has the most mileage.

The upshot

Dropping the import model also dropped the JS toolchain, which was the point. There's nothing to bundle, because there's no component graph to import. The tool is a single Go binary with the UI embedded and zero dependencies. Any backend can sit behind it: it speaks a four-endpoint HTTP protocol, so an app answers those endpoints (there are adapters for Go/templ, Django, Rails and Laravel, and the examples implement the protocol by hand in Python, Node and Ruby to show there's nothing hidden).

It's called Swapbook. It's v0.3, MIT, and still early. Code is at https://github.com/Aejkatappaja/swapbook and there's a live demo at https://aejkatappaja.com/swapbook/. Mostly I'm curious whether the proxy framing resonates with other people building hypermedia apps, or whether I've just rationalized a shortcut.

Top comments (0)