Skip to the content.

The Universal Frontend Architecture: UI, Server, and State as three seams

The Universal Frontend Architecture: UI, Server, and State as three seams

Every frontend, in every framework, does three things: it renders a UI, it talks to a server, and it holds state. React, Vue, Angular, Svelte, and web components differ in syntax, but not in this — the three concerns are universal. The Universal Frontend Architecture is simply the decision to keep those three as separate seams rather than tangling them together. Do that, and your knowledge — and often your code’s shape — survives a framework swap, because you moved the boundaries, not the framework. Tangle them, and every concern leaks into the others, which is the state most codebases drift into by default.

Three seams — UI, State, Server — connected by narrow, explicit boundaries UI renders and emits events; State holds and derives; Server fetches and persists. Arrows show UI reads State via selectors, State talks to Server via effects, and Server data flows back into State. UIrender + events Statehold + derive Serverfetch + persist selectors actions effects data
Three concerns, three seams, narrow boundaries: UI reads State via selectors and reports events via actions; State reaches the Server via effects and absorbs its data back.

Each seam has one job and a narrow interface

The UI layer renders from props and emits events — nothing more. It does not fetch, and it does not know the store exists. The State layer holds the app’s facts, exposes them via selectors, and mutates only through actions. The Server layer fetches and persists, and its results flow into State as data, never straight into a component. The interfaces between them are deliberately narrow:

// UI: pure, reads via props, reports via callbacks — swappable framework, same shape
function Cart({ items, total, onRemove }) {
  return <ul>{items.map((i) => <Row key={i.id} {...i} onRemove={() => onRemove(i.id)} />)}</ul>;
}
// State ↔ Server: the effect talks to the server; data comes back as an action
const loadCart = () => async (dispatch) => {
  dispatch({ type: "CART_REQUEST" });
  dispatch({ type: "CART_SUCCESS", data: await api.getCart() });  // server data → state
};

The Cart component would work, essentially unchanged in spirit, in Vue or a web component, because it only depends on the seam’s contract.

The boundaries are what survive change

The payoff is portability of reasoning. When you separate the seams, a framework migration is a rewrite of one layer’s syntax, not an archaeology dig through tangled concerns. A rendering-strategy change (CSR to SSR) touches the Server seam and leaves UI and State alone. A store swap touches State and leaves UI alone. The value is that each concern can change on its own schedule, which is exactly what a tangled codebase cannot do — there, touching the fetch means touching the component means touching the state, all at once.

Defend the seams, because they erode by default

Left alone, these boundaries dissolve: a component fetches “just this once,” a reducer grows a derived field, server data gets edited as if owned. Each is locally reasonable and globally corrosive — and an AI generating code erodes them faster, since crossing a seam is usually the shortest path. So the architecture is only real if it is defended, ideally with checks: a lint rule that forbids the UI layer importing State, a shape test on the store, a boundary the build enforces. The Universal Frontend Architecture is not a framework or a library; it is the discipline of keeping three unavoidable concerns as three separate seams, and the guardrails that keep them that way. The presentational-vs-container exercise is the UI↔State seam in practice, render-strategy-choice is the State↔Server seam, and design-micro-frontends scales the whole shape across teams.