Journal

How the web stack grew up

The last decade did not produce a winning framework. It produced better runtimes, lighter delivery, shared standards and durable workflows, plus two answers nobody yet ships in the same box: opinionated frameworks and open standards.

All postsArchitectureOpen sourceDeployment
Published
29 July 2026
Read
19 min read
Written by
Eric Chautems
Contents

On this page

The web did not spend the last decade waiting for another framework to win.

That is easy to miss, because we still argue about frameworks as if one of them will eventually settle everything. React or Vue. Next.js or Nuxt. Server components or islands. Serverless or a server after all. The names keep changing, and the interesting work has been happening underneath them. We fixed the runtime. We made the build disappear during development. We learned to ship less JavaScript, to describe an API before implementing it, to follow a request across several services and to resume work after a process dies.

Deno, JSR, Vite, Preact, OpenAPI and OpenTelemetry, each paired with the assumption it removed.Deno, JSR, Vite, Preact, OpenAPI and OpenTelemetry, each paired with the assumption it removed.
FigureNone of these is a framework. Each removed an assumption the previous generation had accepted.

Two things came out of that decade: opinionated, production-grade meta frameworks, and standards that belong to nobody in particular. What has not arrived is one thing holding both, with deployment included and no platform attached to the invoice.

Deno learned that a successor still needs its predecessor

Ryan Dahl speaking at JSConf EU 2018.
FigureRyan Dahl introducing Deno during his talk on the decisions he regretted in Node.js.1

Ryan Dahl introduced Deno by talking openly about the decisions he regretted in Node.js. The replacement was secure by default, ran TypeScript directly, and arrived with the tools most projects had spent years assembling for themselves: a formatter, a linter, a test runner, a compiler that produced one executable.

It was a much cleaner answer. I wanted it to work.

The problem was not the runtime. The problem was the decade of software already sitting on npm. A new project can choose better defaults; a company with an existing application has customers, deadlines and dependencies that nobody is going to rewrite because a runtime has a nicer permission model. Deno could be right and still remain on the outside.

So it changed. Node compatibility improved, npm packages became first-class, and by the time Deno 2 shipped it accepted package.json, workspaces and even node_modules, the directory even its defenders occasionally delete just to see whether things improve. The project built to escape Node became able to run Node projects.

That looks like surrender if the goal is purity. It looks like progress if the goal is adoption. TypeScript had already taught the same lesson: it did not defeat JavaScript, it made JavaScript safer while preserving the enormous body of code people already had.

A successor does not win by erasing the previous generation. It wins by carrying it forward.

Which is also why Node is not going anywhere, and why that is perfectly normal. COBOL still moves money. Markets do not replace foundations on schedule; they add a better path and move at the speed their existing software allows.

Compatibility was only half of the bet. The other half sat below TypeScript. Deno built its runtime in Rust and created rusty_v8, the bindings that let a Rust host embed V8 without moving the application itself into C++. JavaScript stayed the portable surface; Rust owned the code where lifetimes, threads and native memory stop being theoretical.

That choice looks more prescient now than it did at the time. In July 2026, Bun announced it was rewriting its Zig implementation in Rust after years of use-after-free bugs and lifetime problems around JavaScriptCore. Bun is careful not to blame Zig, and it should be: Zig made the original project possible. The conclusion is narrower and more useful. A runtime wrapped around a garbage-collected engine benefits enormously when ownership is enforced by the language underneath it.3

The rewrite also arrived in the most 2026 way possible: Bun used a pre-release model for much of the translation and published the prompt that started it, Claude, rewrite Bun in Rust. The Rust decision is easy to defend. Handing an unreleased model most of a runtime rewrite is the part that deserves the raised eyebrow. If AI is going to write systems code, give it a language that refuses to trust it.

The standard library stopped being a package lottery

The runtime was only the first inherited assumption. The next one was where the code came from.

npm remains one of the great successes of the JavaScript ecosystem. It also grew around Node, CommonJS, and packages that publish generated JavaScript rather than the TypeScript their authors actually wrote.

JSR kept npm compatibility and changed the assumptions. Packages publish TypeScript source and standard ES modules, Deno consumes them directly, Node receives transpiled JavaScript and declarations, and nothing outside Node is treated as a special case.

main.ts
// the registry became a prefix, not a migration
import { join } from 'jsr:@std/path';
import { z } from 'npm:zod';
The JSR listing for @std/path: badges for the runtimes it works with, a 100% JSR score, an MIT licence, 139,795 weekly downloads and 1,738 dependents.The JSR listing for @std/path: badges for the runtimes it works with, a 100% JSR score, an MIT licence, 139,795 weekly downloads and 1,738 dependents.
FigureThe same package on the registry: it declares which runtimes it works with rather than which one it was written for, and the install line is the import.5

The most important owner on JSR may be the least exciting one: @std. Paths, archives, encodings, HTTP utilities and data formats should not begin with a popularity contest between five packages and a forensic review of which maintainer still has publish access. Common code should be infrastructure, not a lottery.

Vite removed work the browser no longer needed us to do

The frontend toolchain was dealing with a different kind of inheritance. Webpack had solved a real browser problem: for years, bundling was the only practical way to turn a forest of source files and incompatible module formats into something a browser could load. Then browsers gained native modules, the original constraint disappeared, and development builds kept bundling anyway.

Vite asked the obvious question: what if we stopped?

A bundle-based development server compared with a native-ESM server that transforms modules on demand.A bundle-based development server compared with a native-ESM server that transforms modules on demand.
FigureThe same modules and the same browser. The difference is how much of the application must exist before the first byte arrives.6

In development it serves source modules and transforms them when they are requested, so the application can grow without turning every start into a full rebuild. Production still gets an optimised bundle. The developer simply stops paying production costs after every keystroke, which is a small architectural decision with an enormous human consequence: we had turned saving a file into a build pipeline and accepted the coffee break as developer experience.

Vite never needed to become a framework to fix that. React, Vue, Preact, Svelte and plain TypeScript projects all took the improvement without changing what they were. The pattern was becoming clear. The successful tools were not asking for another rewrite. They were finding the expensive assumption and removing it.

Preact and Fresh made “less” a feature again

Faster builds fixed the wait on the developer’s machine. They did nothing about what we sent to everyone else’s. For a while the answer to a slow web application was more web application: more client state, more hydration, more caching, another loading state to hide the work. It could feel fast once it had started. Starting it became a job of its own.

Preact kept the component model and most of the API people were already using, then delivered it in a much smaller implementation, which proved that compatibility and restraint are not opposites. Fresh pushed the idea further: pages render on the server, and JavaScript goes only to the parts that must stay interactive. Those parts are islands. The rest is HTML, as the web intended.

A mostly static HTML page with only search and cart marked as interactive islands.A mostly static HTML page with only search and cart marked as interactive islands.
FigureThe question stops being how to hydrate the page and becomes which parts genuinely need JavaScript.7

There is nothing new about sending HTML. That is exactly why it is useful. HTML had been waiting patiently underneath the loading spinner the whole time.

The default question changed from how do we hydrate this page? to which part of this page actually needs JavaScript? Usually, not much of it.

Frameworks brought the decisions back

Sending less is a decision, and someone has to make it. For years every project made that one alone, along with every other one. React won partly because it left so many choices open. The ecosystem that grew around it eventually started closing them again. Next.js, Nuxt, SvelteKit, Astro and Fresh decide where routes live, what renders on the server, how data reaches a page and how much code crosses into the browser. They disagree sharply on the answers, and they agree that an application needs answers. A new project no longer begins with three days of choosing a router, a build tool, a rendering strategy, a test runner and the folder where all of it will be explained to the next developer, who will disagree with half of it. They also followed the lesson Deno had to learn, adding their opinions on top of an ecosystem people had already chosen.

But a web framework sees the system from the page inward. It can decide where an endpoint lives. It rarely decides what happens when that endpoint starts work in three services, the second succeeds, the third times out, and the process restarts before the response comes back. That is not a criticism, it is a boundary. These frameworks solved a much larger part of the application than the libraries before them, and the architecture of the business stayed ours to assemble.

The standards stopped belonging to vendors

Fortunately, we no longer have to invent the language of that architecture.

An API can be described with OpenAPI before a client is written, and that description generates the documentation, the validation and the clients for other teams in other languages. The valuable part is not the generated website with a list of endpoints. It is that the producer and the consumer stop maintaining two versions of the truth. Contract-first moves that agreement to the beginning: the schema is not paperwork produced after the code, it is the boundary the code implements, and a breaking change becomes something a tool rejects rather than something a customer discovers.

One breaking change on two timelines, drawn against a single vertical release line. On the top row, the spec is written from the code: the code, then a client written to match, then a spec generated from the code — after which nothing rejects the change, and the same copper cell for the breaking change lands to the right of the release, found by a customer. On the bottom row the schema is written first: the schema, then the server generated from it, then the clients generated from it, and the identical copper cell lands to the left of the release, rejected by the tool.One breaking change on two timelines, drawn against a single vertical release line. On the top row, the spec is written from the code: the code, then a client written to match, then a spec generated from the code — after which nothing rejects the change, and the same copper cell for the breaking change lands to the right of the release, found by a customer. On the bottom row the schema is written first: the schema, then the server generated from it, then the clients generated from it, and the identical copper cell lands to the left of the release, rejected by the tool.
FigureThe same breaking change, on either side of the release line. Contract-first does not add a check; it moves the schema early enough for one to exist.

Observability followed the same path. Logs, metrics and traces used to begin with a vendor choice; OpenTelemetry gave them a neutral model and a common way to move the data, so instrumentation belongs to the application rather than to whichever dashboard the company is paying for this year. It reached the CNCF’s graduated level in 2026, which matters less as a date than as evidence: shared telemetry is infrastructure now, across languages, platforms and vendors.

The OpenTelemetry Collector: OTLP, Jaeger and Prometheus receivers feed pipelines of batch, attribute and filter processors, which export to the same three backends.The OpenTelemetry Collector: OTLP, Jaeger and Prometheus receivers feed pipelines of batch, attribute and filter processors, which export to the same three backends.
FigureThe Collector keeps instrumentation independent from storage: receivers, processors and exporters can change without rewriting the application.8

I used to think these things belonged in the final hardening phase of a serious project. That phase is usually another name for discovering, too late, that an undocumented contract and an untraceable request are expensive to repair. The contract, the trace identifier and the health endpoint should exist when the first feature is written, not when the first client asks why it failed.

We did not lack standards. We treated them as optional finishing work.

JavaScript teams were not uniquely incapable of using them. They came from an ecosystem that celebrated choosing every piece. In more opinionated platforms, the same capabilities arrived with the framework and disappeared into the definition of a normal application.

Architecture became a question of boundaries, not size

The same decade that made microservices fashionable also taught us to stop treating them as a unit of virtue. Splitting a system can create useful boundaries. It can also turn a function call into a network call, a transaction into a coordination problem and a deployment into twelve deployments. A small service is not automatically independent, and a monolith is not automatically tangled.9

My own vocabulary settled on two larger units. A macro service is a substantial business capability with its own data and a boundary worth operating independently, which the industry would place closer to service-based architecture than to anything the word micro implies.10 Inside it I prefer use case services: code organised around a piece of work the business performs rather than around controllers, repositories and utility layers. Vertical slice architecture is the established term. A change to one use case stays inside one slice instead of travelling through every horizontal layer of the application.

The names matter less than the direction. We stopped asking how small a service could be and started asking which changes should travel together.

Two ways of grouping the same application. On the left, three layers — controllers, application services, repositories — with one change threaded through all three. On the right, one macro service holding four use case slices — submit, approve, publish, notify — with the same change contained in a single slice.Two ways of grouping the same application. On the left, three layers — controllers, application services, repositories — with one change threaded through all three. On the right, one macro service holding four use case slices — submit, approve, publish, notify — with the same change contained in a single slice.
FigureGrouping by layer spreads one change across shared code. Grouping by use case keeps it inside one slice of a capability that owns its data.

Events help when work crosses those boundaries: one capability announces what happened without knowing every reaction in advance. But the diagram is the easy part. The hard part is remembering what has already happened, retrying safely, and repairing a workflow that completed four steps before the fifth failed. None of that is new either. Sagas were described for long-running transactions in 1987,12 and event sourcing, CQRS and idempotency have been documented for years.13 Durable execution gave the runtime a practical way to checkpoint progress, so work resumes after a crash instead of restarting from memory and hope.14

The embarrassing discovery was not that the industry lacked an answer. It was how often we had rebuilt partial answers without using the names that would have led us to the rest. Once the patterns have names they can become part of the platform: a generated service with an idempotency boundary, a workflow that is durable by default, an event that carries a trace context.

One language was never the point

Nothing in that says the code behind a boundary has to be TypeScript. Deno is its own demonstration: TypeScript on the surface, Rust underneath.

Several languages are exceptionally good at different kinds of work, and the honest position is to use them. Go is still the right answer for a small self-contained worker: one compiled program, very little ceremony between the build and the machine. Rust goes further where memory control and native performance matter, and it has become the successor to C for a growing class of systems code, the Linux kernel included. Modern .NET has not disappeared into enterprise history, and P/Invoke remains the most practical way to talk to the good old DLL that quietly runs half a Windows business. The JVM is still formidable for CPU-heavy, highly concurrent work, although apparently even Java cannot negotiate with the number of cores in the machine. Python owns machine learning for good reasons. Bash and PowerShell still solve small operational problems faster than most application frameworks can finish installing.

The danger is not the language. The danger is letting every language bring its own deployment model, configuration, permissions, logs, retries and undocumented way of passing data to the next one.

Polyglot code is not the problem. Uncontracted polyglot code is.

A task can have a declared input and output, a restricted filesystem and network scope, a memory and time limit, a trace identifier, and one place its logs go. Behind that boundary it can be a Go executable, a Rust library, a .NET worker, a Python script or ten lines of Bash. A scoped, sandboxed, monitored shell script is not a legacy burden. A script that inherits production credentials, runs forever and communicates by appending to a mystery file absolutely is.

The old fragmentation came from composing platforms. The better model is to compose tasks, and to treat the language behind each one as an implementation detail.

Deployment became a spectrum

Contracts converged. Deployment did not, because the market never had one requirement.

The cloud removed the need to own hardware. Serverless removed the need to manage a long-running process. Containers made an application and its runtime travel together. Kubernetes gave large organisations one control plane for many containers, teams and machines. Every one of those solved a real problem, and every one introduced an assumption.

Kubernetes is the clearest case: excellent at solving Kubernetes-sized problems, and trouble starts when we enlarge the problem until it qualifies.

The official Kubernetes cluster architecture. A control plane box holds etcd, the kube-api-server, the scheduler, the controller manager and the cloud-controller-manager, which reaches out to a cloud provider API. Two nodes sit beside it, each running a kubelet and a kube-proxy above a container runtime that holds the pods. Arrows converge on the api-server.The official Kubernetes cluster architecture. A control plane box holds etcd, the kube-api-server, the scheduler, the controller manager and the cloud-controller-manager, which reaches out to a cloud provider API. Two nodes sit beside it, each running a kubelet and a kube-proxy above a container runtime that holds the pods. Arrows converge on the api-server.
FigureA key-value store, an API server, a scheduler, two controllers and an agent on every node — before your application starts. Right at cluster scale, an assumption everywhere else.15

But every target on that spectrum has the same shape. Most software does not need to run everywhere, and products do not always get that luxury. A public application may fit a managed platform perfectly, while the same product sold to a hospital has to stay inside its network, the next customer already runs Kubernetes and refuses anything that bypasses it, and the industrial site has no container runtime and no internet connection at all.

At that point deployment is part of the architecture, not the last command in a pipeline. Configuration, storage, service discovery, observability and updates all have to survive the move. An adapter bolted on at the end can translate a build command. It cannot remove an assumption the framework baked into the application on the first day.

Aspire is the clearest attempt at refusing that assumption up front, and it is worth knowing about even if you never write a line of C#. You describe the application’s topology in code — this database, that API, this frontend, and which one depends on which — and a single description serves every environment. Run it locally and it starts the containers and the processes in dependency order, injects the connection strings and endpoints, and gives every service a name the others can resolve by.

apphost.ts
const builder = await createBuilder();

const db = await builder.addPostgres("db")
.addDatabase("appdata")
.withDataVolume();

const api = await builder.addNodeApp("api", "../api", "server.js")
.withReference(db)
.waitFor(db);

await builder.addViteApp("frontend", "../frontend")
.withHttpEndpoint({ env: "PORT" })
.withReference(api);

await builder.build().run();

That same resource graph is what gets published. Ask for Docker Compose and it writes the compose file, ask for Kubernetes and it writes manifests, ask for a managed cloud environment and it provisions one: one graph produces target-specific output without duplicating the topology in each deployment workflow.16 The generated services already carry an OTLP endpoint and a service name, because telemetry belonged to the model instead of being added at the end. And the project arrived from .NET without staying there — the same host orchestrates Node, Python, Go and Java services, and can itself be written in TypeScript.

The same instinct explains why the return of the self-contained executable is more interesting than it looks. One file is not always the smallest deployment, but it is an unusually portable agreement.

terminal
deno compile --allow-net --output ./server main.ts
scp ./server ops@line-01.factory.local:/opt/app/

Go made that agreement familiar again, Deno brought it to TypeScript, and containers offer a similar one wherever a container runtime exists. Deno Desktop began closing the other half of the circle in 2026: a web frontend and a TypeScript backend packaged as one desktop application, on the operating system’s WebView or a bundled Chromium, able to open a native window, talk to the machine and keep its data local.

That matters beyond desktop nostalgia. Productivity tools need files, processes and devices. Air-gapped software cannot move its work to a cloud function. Local-first software should not need a round trip across a continent to open a note. And AI put the question back on the table for everyone: an assistant is far more useful when it can work with the machine than when it can only chat from a tab.

The web did not replace the desktop. It finally learned how to build one.

The mature answer is not to declare one target modern and the others legacy. It is to keep the application independent enough that the target can change without becoming a rewrite.

AI made defaults more important

All of that was in motion when the ground moved again.

Everyone who hated JavaScript could still hope that a better language would eventually become the default multi-platform choice. AI buried that hope. Ask a model for a proof of concept, a simple website, or one of the operating-system clones people like to benchmark, and it will reach for HTML, CSS, JavaScript and very often React. Not out of taste: models learn from what exists, and the web has more examples than anything else.

If AI-assisted coding had stayed a hobbyist habit, that would not matter much. It is now part of daily development, which changes what a convention is for. The decisions a framework supplies are no longer guidance for one developer. They are the rails for every machine asked to extend the project later. Point a model at an empty directory and it will assemble the most statistically familiar pile. Point it at a codebase with an executable contract, a known service shape, durable work and traces that already exist, and its choices become narrower and far easier to review.

AI did not create the discipline problem. It increased the speed at which missing discipline becomes code.

We finally have a foundation

It is tempting to read this history as another list of things the web still gets wrong. I read it the other way.

The runtime can be secure, typed and self-contained without giving up the Node ecosystem. A page can ship JavaScript only where it earns it. Contracts and telemetry can cross vendors and languages. Distributed work can survive failure. The same application can be packaged for a cloud function, a container, or a machine that has never heard of either. Those are not proposals. They are working parts, maintained by serious projects, and they are much better than what we had ten years ago.

What remains is the assembly. Today a good team can pick these parts and build a strong platform: decide how a service is shaped, connect the contract to the implementation, add the telemetry, choose how events and durable work behave, generate the clients, write a deployment path for each environment. Then another good team makes slightly different choices and builds the same layer again.

That repetition is not evidence that the parts failed. It is evidence that they have become a foundation.

Scaffolders exist because that work is so repetitive. Better-T-Stack asks seventeen sections of questions — web frontend, native frontend, backend, runtime, API, database, ORM, database setup, web deploy, server deploy, auth, payments, package manager, addons, examples, git, install — and writes out a working monorepo for whatever you answer.17 It is the honest, unopinionated form of the problem: it does not claim one stack is right, it just gives you back the days you would have spent wiring your own together.

The Better-T-Stack builder. A left rail shows the CLI command bun create better-t-stack@latest and a selected stack labelled eleven picks, with chips for TanStack Router, Hono, Bun, tRPC, SQLite, Drizzle, Better-Auth, bun and Turborepo. A strip of configuration sections runs off the right edge: web frontend, native frontend, backend, runtime, API, database, ORM, database setup, web deploy, server deploy, auth, payments, package manager, addons. Below it the web frontend section offers nine options, from TanStack Router and Next.js to Astro and no web frontend at all.The Better-T-Stack builder. A left rail shows the CLI command bun create better-t-stack@latest and a selected stack labelled eleven picks, with chips for TanStack Router, Hono, Bun, tRPC, SQLite, Drizzle, Better-Auth, bun and Turborepo. A strip of configuration sections runs off the right edge: web frontend, native frontend, backend, runtime, API, database, ORM, database setup, web deploy, server deploy, auth, payments, package manager, addons. Below it the web frontend section offers nine options, from TanStack Router and Next.js to Astro and no web frontend at all.
FigureSeventeen sections of choices, and a working repository at the end. Nothing in the menu is a trace, a contract, or a type that survives the network.17

What a generator cannot hand you is the part that only exists once the pieces are joined. End-to-end telemetry across every one of those choices, types that survive the network boundary, clients generated from a contract, industry standards adopted by default instead of retrofitted: those are properties of the assembly, not of any component in it. No menu of components produces them, however good the menu is.

And that is the whole gap: the frameworks decide the page and stop at the network, the standards decide the boundaries and belong to nobody, and the deployment story is inherited from whichever of the two you started with. Nothing yet hands you all three without asking you to give one of them up.

So the next generation of frameworks does not need to invent another router, another schema format or another deployment platform. It needs to take the standards and patterns we already trust, make them the default, and leave enough of the existing web intact that people can actually adopt it.

That is a much narrower problem than fixing JavaScript. It is also, finally, a problem we know how to solve.

Start with How the web became the default.

Sources

  1. 110 Things I Regret About Node.js — Ryan Dahl at JSConf EU 2018, conference recordingyoutube.com
  2. 2Announcing Stable V8 Bindings for Rust — Official Deno announcement by Ryan Dahldeno.com
  3. 3Rewriting Bun in Rust — Official Bun announcement by Jarred Sumnerbun.com
  4. 4Rewriting Bun in Rust on X — 456 replies · 1.1K reposts · 8.9K likes · 5.1K bookmarks · 3.7M views, captured July 30, 2026x.com
  5. 5@std/path on JSR — Official registry listing, captured July 30, 2026jsr.io
  6. 6Why Vite — Official explanation of native ESM development servingvite.dev
  7. 7Islands — Official Fresh documentationusefresh.dev
  8. 8OpenTelemetry Collector — Official documentation diagram, CC BY 4.0, colour-inverted for the dark themeopentelemetry.io
  9. 9Microservice Premium — Martin Fowler, May 13, 2015martinfowler.com
  10. 10Service-Based Architecture Style — Mark Richards and Neal Ford, Fundamentals of Software Architecture, chapter 13oreilly.com
  11. 11Vertical Slice Architecture — Jimmy Bogard, April 19, 2018jimmybogard.com
  12. 12Sagas — Hector Garcia-Molina and Kenneth Salem, ACM SIGMOD 1987, doi 10.1145/38713.38742dl.acm.org
  13. 13Event Sourcing — Martin Fowler, December 12, 2005martinfowler.com
  14. 14Understanding Temporal — Official documentation on durable executiondocs.temporal.io
  15. 15Cluster Architecture — Official Kubernetes documentation, figure 1, CC BY 4.0, colour-inverted for the dark themekubernetes.io
  16. 16How Aspire deployment works — Official documentation on one resource graph producing target-specific deployment outputaspire.dev
  17. 17Better-T-Stack Builder — Official project scaffolder, captured July 30, 2026better-t-stack.dev

Journal

More from the journal

  • When content management and business workflows meet

    Autocorner’s website became the foundation for something larger: reusable forms, real availability, notifications and internal tools that connect customer journeys with daily dealership work.

    ArchitectureContentCommerce5 min read
  • A true multi-brand and multi-channel experience

    Autocorner needed more than a cleaner website. It needed one platform where every brand, centre, content channel and live vehicle listing could work together without becoming a separate project.

    ArchitectureMigrationCommerce5 min read
  • A simple yet flexible CMS driven website.

    A practice with no developer needed to change its own website at lowest possible friction while being able to build a beatuiful website.

    ContentArchitecture3 min read

Next step

Recognise the problem?

If one of these sounds like your system, the first conversation costs nothing.