The Frontend’s Many Roads, and Why We Take the Server

The previous chapter said the book’s choices follow from a single architectural decision, and promised to make it explicit here. This is it: the server is the authority. Domain state lives in the database, behind a server you control; the browser is a rendering surface, not a source of truth. Every later choice (server-rendered HTML, the URL as state, a strict Content-Security-Policy, no client bundle, progressive enhancement) follows from it.

The argument for it is the history itself – and that history was never one road. Several distinct traditions grew from different problems, each accepting a trade-off to solve its own; once you can see which problem each solved, what it cost, and where it is now heading, the reasons to put the server in charge come into focus on their own. So this chapter follows the why: why the application moved to the browser, why that turned out to be expensive, why some traditions never moved at all, and why, for an app like ours, the server is the right place to stand.

Why the application moved into the browser

Start with the road that made the consequential bet.

React came out of a genuine problem at real scale. Facebook’s UI (news feed, ads, chat) was hard to keep consistent as state changed underneath it; the imperative DOM updates that kept the screen in sync with the data were a swamp of bugs. Jordan Walke’s prototype (around 2011, inspired by Facebook’s PHP component system XHP) became React, open-sourced in May 2013. Why did it win? Because the virtual DOM made one idea cheap that had previously been expensive: treat the UI as a pure function of state, re-render the whole thing on every change, and let a diff reconcile it to the DOM. For large, genuinely interactive applications, that was a real advance, and it deserved its adoption.

But notice the trade-off baked into “the UI is a function of state”: where does the state live? If rendering happens on the client, the most natural place to keep the state it renders from is also the client. That pull, the gravity of its model more than the tool itself, is how the application drifted into the browser. Flux and then Redux (2015, borrowing the Elm architecture’s discipline) answered the question that followed: as the client state grew, teams needed it predictable and debuggable, so they centralized it into a single store, and the server was demoted to a JSON API feeding that store. In ClojureScript the same wave arrived as Reagent (Dan Holmsand’s Hiccup-flavored interface to React) and re-frame (Mike Thompson’s framework on top), whose single big app-db atom is the purest statement of the era: the application’s truth, held in one place, in memory, in the browser.

For complex, locally interactive applications (editors, design tools, canvases) this was correct, and still is. The mistake was never the tool; it was the default. The model was applied to content- and data-shaped apps that had no need of it, and they inherited a trade-off that only makes sense when the client owns the experience: the loss of addressability. Once authoritative state lives only in memory, refresh discards it, the URL no longer describes what you are looking at, back and forward misbehave, and “share this link” breaks, because the link no longer names the state. The web’s native strengths were traded for in-app smoothness. For Facebook’s feed, a reasonable trade. For a recipe page, a bad one.

Why that bet turned expensive – and the war to undo it

What follows is the application lineage spending enormous effort to walk its own bet back.

First came server rendering, bolted on (Next.js for React, Nuxt for Vue), because the empty-shell-plus-JavaScript model hurt what content apps need most: first paint and SEO. But rendering on the server in a client-first framework introduced a new cost of its own: hydration. The server sends HTML, but the HTML is inert – the event handlers and component state live in JavaScript that has not run yet. So the client must download the whole application, re-execute it, rebuild the component tree, and “attach” it to the server’s HTML to make it interactive. You render twice and ship the entire app to light up a page. That is the hydration tax, and it exists only because authority was moved to the client in the first place.

Almost every famous framework development since attacks some part of that bill: what must hydrate, what must be sent, what must re-execute, and how much runtime must ship. Islands (the term coined by Etsy’s Katie Sylor-Miller in 2019, popularized by Preact’s Jason Miller in 2020, and productized by Astro) ask why you should hydrate a whole page when most of it is static, and answer by shipping zero JavaScript by default and reviving only the few interactive components. React Server Components, announced in December 2020 and shipped in React 19, ask why a component’s code should be sent at all if it only needs server data, and so let such components run only on the server while reserving client JavaScript for the islands where interaction is real – React itself moving authority back toward the server. Resumability, in Qwik – from Miško Hevery, the creator of AngularJS, built at a visual-builder company whose generated sites shipped crushing JavaScript – asks why the client should re-execute anything at all, and serializes the state and the listener locations into the HTML so the page resumes instead of re-hydrating. And the turn to signals and compilers (Solid’s fine-grained reactivity, Svelte’s influence, Vue’s runtime-light explorations) asks why a virtual DOM should be diffed at runtime, and compiles updates ahead of time to shrink the runtime that must ship.

Read together, this is a decade of brilliant engineering aimed largely at undoing a single decision – shipping less JavaScript, running more on the server, recovering authority that was given away. The lineage is converging, from the client side, on something close to “render on the server, send a little JavaScript.” Which leaves one question: what if you never gave authority away, and so never needed any of this machinery to get it back?

Why some roads never made the bet at all

Several major tools came from genuinely different problems and never moved authority to the client, and treating them as “SPA frameworks that came back” would be simply false.

Vue (Evan You, first cut 2013, released 2014) came out of heavy AngularJS use at Google Creative Lab, not out of the Angular team. He built it to be progressive and incrementally adoptable, because not every page needs a framework: you should be able to drop Vue onto one HTML page like a sprinkle, or scale up to a full app, and choose per project. It was a dial by design, not a demand.

Svelte came from the opposite end of the spectrum from React, and conflating the two is the specific error worth correcting. Rich Harris built it out of newsroom graphics work: interactive data visualizations at The Guardian and then The New York Times, produced under deadline and dropped into otherwise static content pages. His earlier framework Ractive (2013), and then Svelte (conceived around Thanksgiving 2016), were driven by a single conviction: the frameworks of the day shipped far too much JavaScript for a mobile-web audience. Hence the compiler: if you compile components to small, direct DOM code, the framework can largely disappear at runtime, and there is no multi-kilobyte library to download per interactive widget. Svelte’s native use case was rich interactive components added to pages, much closer to islands than to a client-owned application. The full-application framework, SvelteKit, grew out of that success later; it was the consequence, not the premise. That origin still shows in SvelteKit’s strong progressive-enhancement story, where forms work without JavaScript and are merely enhanced when it loads. The trade-off Svelte accepts is a compile step and a little compiler “magic,” in exchange for shipping almost nothing.

And there is the hypermedia tradition, older than the SPA peak, which never left the server because it never saw a reason to. Basecamp’s Turbolinks (2012, inspired by GitHub’s pjax) intercepted navigations to swap the page body without a full reload; it grew into Turbo (Hotwire) in late 2020, adding Frames for scoped partial navigation and Streams for server-pushed HTML fragments, with Stimulus for modest behavior. In parallel, Carson Gross’s intercooler.js (2013) was rewritten without its jQuery dependency in 2020 to become htmx, reviving hypermedia-as-the-engine-of-application-state: any element can issue a request and swap the returned HTML. Phoenix LiveView (Chris McCord, public around 2019) took the boldest line: the server holds the UI state per connection, diffs the rendered HTML, and pushes minimal diffs over a WebSocket. Why did this road accept its own trade-off, a network round trip per interaction? Because for server-rendered apps, the SPA’s client-state complexity simply was not worth its cost, and a round trip is cheap when the server already owns the truth. None of these “came back” to the server. They were always there.

Why we choose the server

Now the decision follows straight from the trade-offs above. Lay our preconditions next to the history: we own the backend, the app is data- and content-shaped, and it is read-mostly. Those are the conditions under which the motivation that justified client-owned state never applies. We have no Facebook-feed problem driving us to the client, so we are free not to pay the bill that came with it.

Choosing the server therefore buys us, at no extra cost, the very things the application lineage spent a decade trying to recover. State becomes addressable by construction: because truth is the URL plus the server, refresh, deep-linking, back and forward, and sharing a link all work without effort, and we would have to go out of our way to break them. There is one source of truth and one renderer, so there is no client/server model duplication, no hydration mismatch, and no gap between the shape an API returns and the shape a UI assumed; validation, authorization, and rendering all live in one place. Security sits where the secrets are, because the browser receives HTML rather than our data model and our rules, leaving less exposed and so less to get wrong. And there are simply fewer moving parts: no bundler in the request path, no client cache to invalidate, no second build artifact, and none of the recovery machinery above – because there is nothing to recover.

It also lets us make three deliberate breaks, each with a clear motivation and an honestly-named cost. The first is from the in-memory store as truth (Redux’s store, re-frame’s app-db), because that store is what costs addressability. The price we accept is that domain state lives in the database and the URL, so a few interactions an SPA would keep purely local become a server round trip for us. The second is from hydration, because there is no client application to revive: the server renders HTML and the client morphs the next server-rendered HTML into the live DOM, a mechanism built in the morph-dispatcher chapter. The only stateful pieces are small islands, wired and unwired by a one-page controller registry that runs on first load and after every morph; the same chapter introduces the registry, and the progressive-enhancement chapter builds it out. The price is that we build that little piece of lifecycle plumbing ourselves rather than buying a framework that includes it. The third is from the framework runtime and the bundler, because a handful of hand-written ES modules, plus one small vendored morphing library, under a strict, no-eval CSP (see the asset pipeline chapter) is all the client behavior we need; the server’s Hiccup is the single source of markup. The price is that we forgo the rich component ecosystems those runtimes bring.

This is the through-line of the whole history: the application frameworks are solving, at great expense, a problem we decline to create. Server Components, islands, and resumability are ingenious ways to recover server authority after moving it to the client. If you never move it, you never need them.

Several of these roads deserve real respect. Svelte’s compiler instincts and SvelteKit’s progressive enhancement share our values almost exactly; we differ on language and on shipping a framework at all, not on philosophy. Qwik’s resumability is the most serious attack on the hydration tax anyone has mounted. Phoenix LiveView is a sibling – arguably more server-authoritative than we are, since it keeps even ephemeral UI state on the server, at the cost of a stateful socket and server memory per connected client. And in the Clojure world, the community’s own defaults already sit on this road: Biff ships server-rendered HTML plus htmx as its starting stack, Kit reaches the same shape through its official htmx module (its base scaffold is API-first), and Datastar (a newer, server-authoritative library with an official Clojure SDK that streams updates over SSE and drives them with signals) is fast becoming the second pole. So the position this book argues is, in Clojure, the community’s own default rather than a contrarian one; what we add is building the moving parts ourselves so that you own them (the morph-dispatcher chapter weighs that choice against htmx directly). At the radical end, Hyperfiddle’s Electric is a genuinely different bet: a reactive compiler that erases the client/server boundary entirely. We do not take it – it is heavier and more statefully-connected than we want – but it is the kind of break worth admiring. We choose the simplest expression of server authority, not the only one.

Why – and when – you would choose differently

The price of server authority is that every interaction is a round trip. Morphing, prefetching, and optimistic updates hide most of the latency, but for high-frequency, latency-critical direct manipulation (dragging nodes on a canvas, a spreadsheet recalculating as you type) a round trip per keystroke is the wrong model, and no amount of cleverness changes that. You also maintain a little plumbing yourself; rich offline and real-time collaboration are reachable only as islanded features with their own sync; the discipline is cultural rather than enforced by a compiler; and it is a less common skill set than React, which is a real hiring and community cost.

And there are whole categories where a single-page app is simply correct, for the mirror image of the reason server authority is correct for us: the client, not the server, is the only place authority can live. That happens in three broad ways.

The first is that you do not control the backend. Building against third-party APIs, a headless CMS, or a public API you do not own, with no server of yours to render on, leaves the client as the only place your app’s state can be assembled. A SPA is then the right call, and client-owned state stops being an antipattern, because the client is the only authority you have.

The second is that the product is app-shaped rather than document-shaped. By app-shaped we mean software whose value is the interaction itself: an editor, an IDE, a design tool, a spreadsheet, a collaborative canvas. The screen is the application rather than a window onto server-held records, and the working state (the current selection, the drag in progress, the undo stack, the cursor position) is inherently local and changes many times a second. Even with a backend you completely control, that state has to live on the client, because its interactions are latency-critical and round-tripping every keystroke to the server would be the wrong model; the server’s job narrows to persistence and sync. Building such a product on server authority would be as wrong as building our content-shaped SaaS as a from-scratch SPA.

The third is that the product is offline-first: it must keep working with no server reachable, so the client owns the working state outright and reconciles when the connection returns.

What unites the three is that authority cannot, or should not, sit on the server – so the client holds it. The same reasoning that places us on the server places these on the client. Server authority is right when you own the backend and the app is data-shaped; a SPA is right when you do not, or it is not.

The lesson, and the position

The history points to one thing in particular: one tradition made one bet (that the application belongs in the browser) to solve a real problem of large interactive UIs, and a decade of extraordinary engineering has gone into paying that bet down for everyone else who inherited it by default. The other roads (hypermedia, compile-to-minimal-JS, the progressive frameworks) mostly never made the bet, and so never owed the debt. If you own your backend and your app is shaped like data, you can simply decline it, and most of that decade’s churn was never your problem.

This book teaches the server-authority road thoroughly, for two reasons: it is the right call for the database-backed application we build here, and it is the road the application lineage skipped by reflex and is now rediscovering the hard way. It does not claim to be universal. The best-engineering stance the whole book argues is not a camp; it is a question you answer on purpose, having seen everyone’s motivations and trade-offs laid out: know which road your project is on, choose it deliberately, and be honest about what it costs. We put ours on the server – and the rest of the book is what that decision makes possible.

Sources and further reading

The historical claims above are drawn from primary and secondary sources: