Skip to content

Chapter 1: The Web Was a Document System

The web started as a way to share documents.

In 1989, a researcher named Tim Berners-Lee, working in a corner of CERN that mostly housed particle physicists, wrote a memo proposing a way to link the lab’s scattered documents together. The proposal was technical enough to be useful and small enough to actually build. By 1991 the first website was live. By 1993 a graphical browser — NCSA Mosaic, written by Marc Andreessen and Eric Bina — had made hypertext something a person could click. By 1995 the web was the fastest-spreading software platform in history. None of that was an accident. It was design — extraordinarily good design, by people working at the edge of what the available technology could do.

The first browsers were document viewers. The first servers were document stores. Hypertext was a way to connect one document to another. Forms were a way to send some text from a reader back to a server. The whole system was designed around the idea that you’d request a document, read it, follow a link to another one, and occasionally submit information through a form.

This was a smaller idea than “application platform,” and the smallness is part of what made it work.

A document model has natural answers to questions an application model has to invent. Where am I? At the URL in the address bar. How do I go somewhere else? Click a link, or type a new URL. How do I send information? Fill out a form and submit it. How do I go back? Press the back button. What happens if I refresh? The browser asks the server for the document again. None of these answers required any code on the page. They were part of the platform.

The early web couldn’t do many things modern applications take for granted. It couldn’t update part of a page without reloading the whole thing. It couldn’t keep client-side state across navigation without help from the server or the URL. It couldn’t render rich custom interfaces. It couldn’t do anything at all if JavaScript was missing or hadn’t loaded yet.

What it could do, it did with real structural integrity. URLs identified resources. The browser owned navigation, history, focus, and the back button. Forms submitted user intent in a structured, predictable way. Pages worked before JavaScript loaded, and they survived if it crashed. Search engines could read them. Screen readers could navigate them. Anyone could bookmark them or share them with a link.

Most of the history that follows in this book is the story of giving the web more capability than this original model could support. The same form that worked well for “save this contact” couldn’t give you a chat window with live updates. The same link-and-reload navigation that worked for browsing a catalog couldn’t give you the smooth feel of a desktop tool. So we built. The frontend stack we use today is the result.

To understand that stack, we need to understand what it was building on top of. That starts here.

The first mental model for the web was the resource.

A resource was anything that could be addressed and returned. A document, an image, a project page, a list of references, a form, a search result, a confirmation page — all resources. They were addressable, requestable, linkable, cacheable, and indexable. They had identity, and they had a place. The URL was the name of that place, and it was every bit as much a part of the user interface as anything rendered on the screen.

When you clicked a link, the browser didn’t need the page to ship a routing library. The link itself was the transition:

<a href="/books/the-left-hand-of-darkness.html">The Left Hand of Darkness</a>

That single line does a lot of work. The browser treats it as an interactive control, so the keyboard can tab to it. The accessibility layer reports it as a link with a name and a destination. It participates in middle-click and right-click menus. It can be opened in a new tab, copied, or crawled by a search engine. It works before any JavaScript loads, and it keeps working if all the JavaScript on the page crashes. None of that behavior had to be written. It was supplied by the platform the moment the markup existed.

Forms work the same way. A form was the web’s way of expressing user intent:

<form action="/books/save" method="post">
<label>
Title
<input name="title" required>
</label>
<label>
Author
<input name="author" required>
</label>
<button type="submit">Save book</button>
</form>

That form encodes a real transaction. The <form> element groups its controls and gives them a single submission target. The <label> elements bind text to inputs in a way assistive technologies can read. The required attribute expresses a constraint the browser will enforce before submission. The submit button declares the action. The action and method attributes tell the browser where and how to send the collected data. The browser handles serialization, the network request, the navigation to the response, and the entry in the history stack — all without a single line of JavaScript on the page.

Modern frontend has spent a lot of energy rebuilding pieces of this. Component libraries that wrap form primitives, state containers for form data, action dispatchers, client-side validation engines, fetch wrappers, optimistic update libraries — most of them exist for good reasons that we’ll come to in later chapters. A lot of what they’re rebuilding was free in the original model.

The document web can’t handle everything modern applications need to do. Products today have requirements the original model never had words for. Even so, the original architecture is still load-bearing thirty years later — links, forms, and URLs are the foundation every modern web stack still rests on, including the ones that work hard to obscure them.

When the user moved from one place to another, the browser handled the transition. Clicking a link meant navigation. Submitting a form meant navigation. Refreshing meant requesting the current resource again. Going back meant returning to the previous one. The loading indicator, the history stack, the address bar — all of those belonged to the browser, not to the page.

This was limiting in obvious ways. A full page reload between every interaction is a poor experience for software that wants to feel like a tool. The white flash, the flash of unstyled content, the lost scroll position, the lost form input — those were real costs, and they were a major reason the industry eventually moved toward Ajax, single-page applications, and client-side routing.

The browser owning the transition was also clarifying. Application state lived in three places: the URL, the current document, and whatever durable state lived on the server. There was almost nowhere else for it to live. If you copied the address bar and sent the link to someone else, that other person would usually arrive at the same place. If the page crashed, refreshing would ask the server for the representation again. If the user wanted to go back, the browser knew exactly what that meant.

That arrangement gave the document web a clean lifecycle. A page loaded, became interactive, was used, and then was torn down by navigation to the next page. Every page was a fresh start. Memory leaks were less likely to accumulate across hours of use because the browser regularly swept the slate. Client-side state didn’t drift forever, because there was very little client-side state to drift. The server remained the authority. The document remained the unit of interaction.

Modern applications often need something more fluid than that, and a great deal of frontend evolution since 2005 has been about pulling these responsibilities — navigation, history, lifecycle, transitions — into the client so they could be made smoother and more responsive. That move bought real things, and it gave up real things. One of the central proposals in the second half of this book is that the platform has caught up enough that we can give some of these responsibilities back to the browser without losing the smoothness that originally pushed us to take them away. The shape of what the document web gave us for free was a clear lifecycle, addressable state, predictable transitions, graceful fallback when something went wrong, and a meaningful boundary around each user action. A lot of what frontend frameworks do today is put those properties back, in code.

The early web’s power came partly from a bet that documents should describe meaning, not just appearance. It was a deliberate bet, made by Berners-Lee in the original HTML design and elaborated by the working groups that came after, and it has paid off in ways the original designers couldn’t have predicted.

Take a heading. The browser renders it larger than body text, but the visual treatment is a consequence of what a heading actually is: an element that marks a structural division in the document. The same is true of lists, buttons, navigation regions, sections, and the rest of HTML’s semantic vocabulary. The markup described what each part of the document was for, and the rest of the platform — the rendering engine, the accessibility tree, search engine crawlers, the cascade — used that information.

Consider this small piece of HTML:

<h1>My Library</h1>
<nav>
<a href="/books">Books</a>
<a href="/authors">Authors</a>
</nav>
<main>
<section>
<h2>Recently Added</h2>
<ul>
<li><a href="/books/1">A Wizard of Earthsea</a></li>
<li><a href="/books/2">Parable of the Sower</a></li>
</ul>
</section>
</main>

Visually, that’s a heading, some links, and a list. Semantically, it’s a document with a clear hierarchy — a top-level heading, a navigation region, a main content region, a section with its own heading, and a list of links inside that section. That structure is read by every layer of the platform: browsers render it, screen readers navigate by region and heading level, search engines extract it, the cascade targets it, keyboard users tab through it. None of that required additional attributes, ARIA roles, or framework code. The semantics were the markup.

This is one of the patterns the rest of the book will keep returning to: when we discard platform semantics, we inherit platform responsibilities. If a clickable element is a <button>, the browser already knows it should be focusable, that Enter and Space should activate it, that it should have an accessible name and role, that it should participate in form submission, and that it should respect the disabled state. If the same element is a <div> with an onClick, all of that has to be reimplemented, and a great deal of frontend bug-fixing across the last decade has been about reimplementing it badly.

The early web’s semantics weren’t enough to build every modern interface. A <dialog> element didn’t exist for most of the web’s history. A serious component model had to wait for custom elements. The principle, though, was already there: HTML is meant to mean something, and that meaning is what makes the platform’s other layers — the browser, the accessibility tree, search engines, the cascade — able to help.

Links and forms divided the document web into transitions and actions. A link said “take me there.” A form said “do this.” Both were declarative. Both were handled by the browser. They answered different questions, and the form answered the harder one.

A form is an explicit statement of user intent. The user assembled some values, named the action they wanted (“save book”, “submit application”, “log in”), and asked the server to take them. The form gave that intent shape — which fields were involved, which were required, where the result should be sent, how. The browser’s job was to package the intent as a request the server could understand. The server’s job was to do the work and return a new representation.

That arrangement is older than most of the architectural patterns this book will discuss, and it still maps cleanly onto a lot of them. A form is essentially a transaction — a bounded scope around a user action with a clear commit point. A submit handler is the action’s executor. A redirect after submission is the system telling the user “here is your new state.” Modern frontend has a long list of names for these ideas — actions, mutations, commands, intents, server functions — and most of them are recreating the form’s original architecture in a richer setting.

The risk in moving past forms is that we keep the richer setting and lose the architecture. A custom field component built with a <div> instead of an <input> doesn’t participate in FormData and may not have an accessible label. A button that calls onClick instead of submitting a form doesn’t trigger native validation, doesn’t get activated by Enter from inside the form, and doesn’t show the right cursor on hover. A validation message rendered next to a field but not associated with it through aria-describedby exists for sighted users only. None of these failures are inevitable — the platform makes the right behavior available — but they’re easy to lose when the form is no longer being treated as the unit.

Forms will return as a serious topic later in the book. We’ll see how to keep their semantics while adding the optimistic updates, inline validation, and progressive enhancement that modern products need. The form’s original design — a structured statement of user intent, with clear submission, validation, and response — encodes a lot of architecture worth keeping.

The URL is one of the strangest pieces of the web’s architecture. It’s a technical identifier — the address of a resource on a server somewhere — and it’s also a piece of the user interface. It’s something the user can copy, paste, edit, bookmark, or share. It tells the user where they are. It tells the search engine what’s worth indexing. It tells the browser what to load when the page is refreshed or revisited.

In the document web, the URL and the document were tightly coupled. If you were on /books/42, you were looking at the page for book 42. If you were on /books/42/edit, you were editing it. If you submitted the edit form and the server redirected you to /books/42, the URL change was the visible shape of the transition. The URL was the application’s primary state, made visible.

Modern applications sometimes preserve this and sometimes don’t. The single-page application architecture, in particular, made it possible to have a great deal of meaningful state that the URL knows nothing about: the current modal, the active filter, the selected row, the open panel, the in-progress draft. Some state genuinely is local, temporary, or too detailed for an address. The cumulative effect of moving state out of the URL, though, is that the address bar becomes a worse and worse representation of where the user actually is.

The user notices. They notice when they refresh and the modal they were in disappears. They notice when they copy a link to share with a colleague and the colleague arrives at a blank dashboard. They notice when the back button closes the entire app instead of the panel they just opened. They notice when a deep link from an email lands them at a generic landing page rather than the specific record they were trying to reach. Each of these is a small failure on its own. Together they tell the user that the application doesn’t quite know where it is.

Addressability is an application feature, not a leftover from the document era. State that matters to the user as a place — what they’re viewing, what they’re editing, what they’re filtering — should usually be reflected in the URL. The chapters that deal with the runtime architecture in Part IV treat addressability as a first-class concern: routes, surfaces, and entities that participate in the URL by default rather than as an afterthought.

The early document web couldn’t do everything we now expect from applications. There was no native-feeling page transition, no sophisticated client-side state, no rich custom widgets, no offline mode, no client-side data synchronization, no real component model. The platform was constrained by slow networks, inconsistent browsers, limited APIs, and a mental model centered on whole-page navigation. Building a modern product on it directly would be painful and, for many products, impossible.

It got several deep things right anyway, and those things were not accidents. They were deliberate architectural choices made by people thinking carefully about what a globally distributed information system needed to be. State was addressable through URLs. Transitions were explicit through links and forms. Documents carried meaning through semantic HTML. The browser was responsible for navigation, history, focus, loading, and lifecycle. Pages could load, render, and become useful before any large JavaScript application initialized. The server was a natural authority for durable state. Many interactions were inspectable, because they were declared in markup you could read.

These are architectural concerns, and they show up, in different forms, in every serious frontend system that’s been built since. A lot of the work that goes into modern applications is the work of recreating, in code, the properties the document web provided in the platform.

Romanticizing this would be a mistake. We can’t build every modern product on form posts and full-page reloads, and the chapters that follow will look honestly at what was missing. Treating the old model as irrelevant would be a different mistake. The question this book keeps asking is: what was durable in the old model, what needed to evolve, and what got accidentally broken in the evolution?

The pressure on the document model came from several directions at once. Users wanted richer experiences and faster feedback. Businesses wanted to deliver software through the browser instead of as installed applications. Developers wanted interactions that didn’t require a full server round trip and a white flash of the screen. The browser of the late 1990s and early 2000s didn’t yet have the APIs to make any of that easy.

So the industry started to add layers. CSS gave us a separation between structure and presentation. JavaScript gave us behavior beyond what links and forms could express. Plugins like Flash gave us rich interactivity before the browser could provide it natively. Ajax let us update parts of the page without reloading the whole thing. jQuery smoothed over the differences between browsers that were drifting in incompatible directions. Structure libraries like Prototype and Backbone tried to bring some discipline to the resulting mess. MVC frameworks like Angular and Ember tried to do that more thoroughly. React reframed the problem entirely as a function from state to UI. Meta-frameworks like Next.js and Remix put server rendering, routing, and data loading back into a coherent package after the SPA era had pulled them out.

That stack didn’t arrive all at once. It accumulated, layer by layer, each new one solving a real problem the previous layer had created or exposed. None of these answers was wrong for its moment. The moments piled up, though, and the cumulative effect is the modern frontend — a deep stack of answers, most of them still in use, most of them still solving the problems they were originally built for, some of them solving problems that have quietly disappeared underneath them.

Each of those layers was serious work by serious people. The list reads like a stack of tools, and it’s also a list of careers, controversies, conferences, and breakthroughs — Brendan Eich on JavaScript, John Resig on jQuery, Jordan Walke on React, Evan You on Vue, Ryan Carniato on Solid, Rich Harris on Svelte, and many more we’ll meet by name. The chapters ahead trace this accumulation in some detail, because the only way to evaluate which of these answers still fit today is to understand what each was originally answering — and the only way to honor the work is to take it seriously.

After that, in Part II, we turn to the moment underneath all of it: the browser quietly catching up. That’s what lets us ask the question this book is built around.

What if we were starting today?

Build a small multi-page application using only HTML links and forms. No JavaScript yet.

A personal library works well for this:

library/
index.html
books.html
book-1.html
new-book.html
saved.html

A suggested structure:

  1. index.html — a heading, a short introduction, and navigation links to the books list and the “new book” form.
  2. books.html — a list of books, each linking to its own page.
  3. book-1.html — book details, a link back to the list, and a link to add another book.
  4. new-book.html — a form with title, author, notes, and a submit button. Use real <label> elements, mark required fields with required, and point the form’s action at saved.html.
  5. saved.html — a confirmation message and a link back to the book list.

Once it’s built, use it the way a real user would. Navigate with the mouse and the keyboard. Use the back and forward buttons. Refresh pages. Copy and paste URLs into a new tab or another browser. Submit the form. Try submitting it with a required field left blank. Inspect the document structure in devtools. If you have a screen reader or accessibility tree inspector available, use that too.

Then sit with these questions:

  1. What behavior did the browser give you without any JavaScript on the page?
  2. Which pieces of the application’s state were represented in the URL?
  3. Which interactions were links, and which were forms? Why?
  4. What would be lost if this application became a single <div id="app"> rendered entirely by JavaScript?
  5. What would become better if the application were enhanced with JavaScript? What kinds of interactions need it, and which ones don’t?
  6. Which parts of this model still belong in modern frontend applications, even ones much more complex than this?

The goal is to make the browser’s original application model visible — to feel, directly, what’s already there before any code on the page asks the browser to step aside, and to feel, concretely, the platform the next round of frontend invention is going to be built on. Before we add layers, it helps to see what we’d be adding them on top of. Whatever the future of frontend looks like, it will rest on the substrate this exercise puts in your hands.