Chapter 11: MVC Comes to the Browser
At a certain point, the browser stopped hosting interactive pages and began hosting applications.
The shift was visible by 2010. Gmail and Google Maps (Chapter 8) had shown what was possible. Ajax had become routine. jQuery had made DOM ergonomics easy. The libraries in Chapter 10 had given developers classes, modules, and widget toolkits. The browser was now serious enough to run software, and the software it was running was getting complicated enough to need real architecture.
A few widgets on a page could be managed with selectors and plugins. A full client-side application needed something more. Data models. Views. Controllers. Templates. Routing. Synchronization. Validation. Lifecycle. A way to reason about state across more than one interaction. The browser didn’t provide that architecture. Frameworks did.
The four names worth knowing are Backbone, Knockout, AngularJS, and Ember. Each one shipped between 2010 and 2013. Each one tried to give frontend code the structural discipline that desktop and server applications already had. Each one had a different design philosophy, a different community, and a different commercial trajectory. Collectively, they’re the moment frontend development became recognizably application development.
The MVC Inheritance
Section titled “The MVC Inheritance”The pattern these frameworks were borrowing wasn’t new.
Model-View-Controller was introduced by Trygve Reenskaug, a Norwegian computer scientist working at Xerox PARC in 1978–1979, as part of the design of Smalltalk-80. Reenskaug’s original conception split a graphical application into three responsibilities. The model held the data and the rules. The view presented the data to the user. The controller mediated between user input and model changes. The three parts communicated through a defined set of relationships, and each part could be modified without rewriting the others.
The pattern spread. It became the structural basis of the Mac’s Cocoa framework (and before that, NeXTSTEP). It shaped the GUI toolkits of Smalltalk, Java’s Swing, Microsoft’s MFC, and most of the desktop application frameworks of the 1990s and 2000s. By the time the browser-based framework generation arrived, MVC was the most widely-deployed application-architecture pattern in working software.
The browser frameworks of 2010–2013 weren’t inventing MVC. They were importing it. The implementations were specific to the browser’s constraints — the DOM was the view, the URL was a routing surface, the lifecycle was tied to page loads and AJAX requests — but the conceptual move was a transplant from desktop application design. Anyone who had built a Cocoa application in 2005 would recognize Backbone in 2011.
Backbone and Jeremy Ashkenas
Section titled “Backbone and Jeremy Ashkenas”Backbone is the cleanest example of MVC in this group, and the smallest.
Jeremy Ashkenas, working at DocumentCloud (a non-profit project aimed at making documents accessible to journalists, originally hosted by The New York Times and now ProPublica), released Backbone 0.1.0 in October 2010. Ashkenas was already known in the JavaScript community for two earlier projects. CoffeeScript (2009) was a small language that compiled to JavaScript, designed to address what Ashkenas considered JavaScript’s syntactic ugliness; it influenced a generation of language design and was eventually largely superseded by ES2015 features and TypeScript. Underscore (2009) was a utility library — each, map, filter, reduce, and so on — that became the substrate for jQuery alternatives and was eventually absorbed into the language itself.
Backbone was Ashkenas’s third major contribution. It was small (the original 1.0 was about 1,500 lines of code) and made a deliberate point of being un-opinionated. The library provided four base classes — Model, Collection, View, and Router — and a handful of utilities. Everything else was up to the developer.
A Backbone model might look like this:
var Todo = Backbone.Model.extend({ defaults: { text: '', completed: false }, toggle: function() { this.set('completed', !this.get('completed')) }})
var Todos = Backbone.Collection.extend({ model: Todo, url: '/api/todos'})A view bound to a model:
var TodoView = Backbone.View.extend({ events: { 'click .toggle': 'toggle' }, initialize: function() { this.listenTo(this.model, 'change', this.render) }, toggle: function() { this.model.toggle() }, render: function() { this.$el.html(template(this.model.toJSON())) return this }})The library’s minimalism was deliberate. Backbone didn’t tell you how to render views (you used Underscore templates, or Handlebars, or your own templating choice). It didn’t tell you how to manage application state above the model level (you wrote your own). It didn’t have opinions about routing details, dependency injection, or testing. It gave you the four primitives and got out of the way.
The trade-off was that Backbone applications, at scale, often invented their own structure on top of the primitives. Two Backbone applications by two different teams might look almost nothing alike. The framework was a foundation, not a way of life.
Backbone was used by Trello (which Atlassian eventually acquired), Foursquare, Airbnb, Disqus, the original Hulu interface, and a substantial fraction of the small-to-mid-sized JavaScript applications built in the early 2010s. The library is still maintained as of 2025, though its active development has slowed considerably and most new projects choose something else.
Knockout and Steve Sanderson
Section titled “Knockout and Steve Sanderson”Knockout took a different angle.
Knockout.js was created by Steve Sanderson, then a Microsoft developer, with the first public release in July 2010. The framework’s design centered on MVVM — Model-View-ViewModel — a variant of MVC that had originated at Microsoft for WPF (Windows Presentation Foundation) and Silverlight. In MVVM, the view binds declaratively to a ViewModel object, and the framework keeps the view in sync with the ViewModel automatically through observable properties.
A Knockout ViewModel:
function TodoViewModel() { this.text = ko.observable('') this.completed = ko.observable(false) this.toggle = function() { this.completed(!this.completed()) }}
ko.applyBindings(new TodoViewModel())The HTML used data-bind attributes to declare bindings to the ViewModel:
<input type="checkbox" data-bind="checked: completed"><span data-bind="text: text, css: { done: completed }"></span><button data-bind="click: toggle">Toggle</button>Knockout’s central technical contribution was observable properties — JavaScript values that the framework could subscribe to and react when they changed. When a ViewModel’s completed observable was updated, every binding that referenced it updated automatically. This was the same pattern Cocoa called Key-Value Observing, applied to the browser. It was also a direct ancestor of the reactivity systems that Vue, Solid, and Svelte would later refine.
The MVVM pattern itself, and Knockout specifically, came out of Microsoft’s developer community. Sanderson’s blog and book — Pro ASP.NET MVC, with successive editions through the 2010s — were the standard reference for the C#/.NET frontend developer through the mid-2010s. Knockout was particularly heavily adopted in enterprise .NET applications, where the MVVM pattern was already familiar from WPF.
Knockout’s adoption faded as Angular and then React took over the broader market. The framework is still maintained, used in legacy applications, and has a small but stable community.
AngularJS and Misko Hevery
Section titled “AngularJS and Misko Hevery”AngularJS — the original Angular, before the 2016 rewrite — was the most ambitious of the four.
Misko Hevery, an engineer at Google, started Angular in 2009 as a side project. The original goal was a framework that let developers write web applications faster, with less boilerplate, by having the framework do as much as possible automatically. Hevery’s design provided declarative templates extended with directives, two-way data binding, dependency injection, a service architecture, custom directive support, and a router — all in one package.
The famous AngularJS 1.x template syntax used custom attributes and double curly braces:
<div ng-controller="TodoCtrl"> <input ng-model="newTodo"> <button ng-click="addTodo()">Add</button>
<ul> <li ng-repeat="todo in todos"> <input type="checkbox" ng-model="todo.completed"> <span ng-class="{done: todo.completed}">{{ todo.text }}</span> </li> </ul></div>The corresponding controller:
angular.module('app', []).controller('TodoCtrl', function($scope) { $scope.todos = [] $scope.newTodo = '' $scope.addTodo = function() { $scope.todos.push({ text: $scope.newTodo, completed: false }) $scope.newTodo = '' }})Angular’s defining feature was two-way data binding. Whenever the user typed in the <input>, the newTodo value in the controller updated automatically. Whenever the controller modified $scope.todos, the <ul> re-rendered. The framework tracked both directions of the relationship and kept them synchronized through a mechanism called the digest cycle — periodic checks of the scope graph to detect changes and propagate updates.
The digest cycle was Angular’s most loved and most criticized feature. It made simple applications easy to write. It made complex applications hard to performance-optimize. The phrase “why is my Angular app slow” in Stack Overflow has, by 2015, become a category of question with a fairly standard answer: too many watchers in the digest cycle, scope graph too deep, dirty checking running too often.
Angular’s commercial weight was significant. Google built and shipped major products on Angular (Google Cloud Console, parts of Google Ads, internal tools). Enterprise teams adopted it widely. The framework had institutional backing the others mostly didn’t.
Angular’s biggest moment, in retrospect, was its breaking change.
The AngularJS-to-Angular Parable
Section titled “The AngularJS-to-Angular Parable”In September 2016, Google released Angular 2.
Angular 2 was a complete rewrite. The framework was written in TypeScript. The template syntax changed. The component model replaced the controller-and-scope model. The dependency injection system was redesigned. Many concepts (services, directives) survived in modified form; others (the digest cycle, $scope, the original module system) were gone entirely. There was no automatic migration path. An AngularJS application couldn’t be upgraded to Angular 2 except by being rewritten.
The community’s response was unhappy. Teams that had built years of work on AngularJS were faced with a choice. Stay on the original AngularJS — which Google would continue to maintain for a transition period, but which had no future — or rewrite the application in a new framework that was, in many practical respects, a different product with a similar name.
A substantial fraction of the AngularJS community chose to leave entirely. React (Chapter 13) was a major beneficiary. Many AngularJS applications that needed to be rewritten anyway took the opportunity to move to React rather than to Angular 2. Vue (Chapter 14), which had been growing quietly through 2014–2016, was another beneficiary — Evan You had built Vue partly in response to his AngularJS frustrations, and Vue’s design felt familiar to AngularJS developers while avoiding the rewrite trauma.
Google supported the original AngularJS through end-of-life in early 2022. Angular (the modern framework, no version-1 suffix) continued to develop and is now on version 19 as of 2025. It remains a major framework, particularly in enterprise environments. It never recovered the dominance the original AngularJS had between 2013 and 2015.
The AngularJS-to-Angular transition is the canonical framework breaking change parable for this book. The pattern is worth landing as a working principle.
A framework that takes a strong opinion about how to build applications creates code that is shaped by that opinion. When the framework changes its opinion — through a major rewrite, a paradigm shift, or just the natural evolution of design taste — the code shaped by the previous opinion has to change with it. The maintenance burden of staying current with a framework is real, and it scales with how much the framework owns. A framework that decorates the platform (Chapters 14, 17 — Vue, Solid, Svelte, Lit, the platform-first approach) imposes less of this burden because the platform itself doesn’t change in the same way. A framework that replaces the platform (Angular 1, Angular 2, the React class-components-to-hooks transition, the various Next.js major versions) imposes more.
This is one of the threads the rest of the book builds on. The browser platform is committed, by treaty, to not breaking what it ships. The frameworks above the platform are not. The longer the time horizon of an application, the more this difference matters.
Ember and the Convention Approach
Section titled “Ember and the Convention Approach”The fourth framework in this chapter took a different position on the same problem.
Ember.js was started by Yehuda Katz and Tom Dale in 2011, evolving out of the earlier SproutCore framework. SproutCore had been used by Apple for the original web version of MobileMe (later iCloud) and was a serious attempt at a Cocoa-style application framework for the browser. Katz and Dale, who had been core SproutCore contributors, extracted what worked and reorganized it into a new framework with cleaner ergonomics.
Ember’s central design choice was convention over configuration. Where Backbone was minimalist and AngularJS was opinionated about specific patterns, Ember made strong assumptions about how an application should be structured and rewarded developers who followed them. Routes, controllers, models, templates, and components all lived in specific places with specific naming conventions. The CLI generated the right files. The framework knew how to wire everything together as long as the conventions were respected.
This was a deliberate design trade-off. Ember applications, by a wide developer consensus, were easier to maintain and scale than less-opinionated frameworks because the structure was uniform. Two Ember applications built by different teams looked similar. New developers could orient quickly. Long-term maintenance benefited from the predictability.
The cost was that Ember was harder to adopt incrementally. A team couldn’t drop Ember into one corner of an existing application the way they could drop in jQuery. Ember wanted to own the whole application, and the convention story didn’t work as well if you were trying to mix it with other patterns.
Ember’s commercial position is interesting because it never had a major corporate sponsor in the way Angular had Google. The framework is run by Tilde Inc., a small company Katz and Dale founded, and supported by a community-led nonprofit. Ember has been used by LinkedIn (which is now Microsoft), Discourse (the forum software), Square, Apple Music’s web client, and a number of other significant products. The framework is still actively developed; it just never crossed into the kind of mass-adoption Angular and React achieved.
Ember is also notable as one of the most successful examples in this book of an independent open-source framework that has stayed institutionally independent over a long period. The Vue (Chapter 14) and Svelte (originally — now at Vercel) trajectories are the closest parallels.
Client-Side Routing as a New Responsibility
Section titled “Client-Side Routing as a New Responsibility”One pattern these frameworks established together is worth naming on its own.
Before the MVC framework era, routing was a server concern. The browser requested a URL; the server returned the document for that URL; the browser navigated there. Frontend code didn’t have to understand the relationship between URL state and application state because the URL change was, by definition, a full page load.
The single-page application model broke this. If the page didn’t reload when the URL changed, something else had to decide what the URL meant. Each of the four frameworks in this chapter shipped a router — Backbone’s Router, AngularJS’s ngRoute, Ember’s robust router, Knockout’s plugins for similar work — and each one provided a way to map URL paths to application state.
The implementation details differed. The conceptual move was the same. The client became responsible for navigation in a way it had never been before. URL change events had to be intercepted and handled. History entries had to be managed (with pushState and replaceState, added to the browser’s History API around 2010). Browser back-and-forward buttons had to be reconciled with the application’s state. Deep links had to work. Sharing a URL had to actually share a meaningful state.
This is one of the book’s most consequential threads. The chapters in Part II include a major architectural argument that routing belongs to the server — that the client-side routing model, while practical and well-engineered, took on a responsibility that the browser already handled cleanly. The MVC era is where the field made the trade. Part II is where the book asks whether the trade was right.
The Framework as Runtime
Section titled “The Framework as Runtime”The deeper observation about all four frameworks in this chapter is that they didn’t ship as libraries. They shipped as application runtimes.
A library is something you call into. The library does a specific job, and your code remains the structural center of the application. A runtime is something you build inside. The runtime owns the lifecycle, the rendering, the state, the events; your code lives inside its conventions and follows its rules.
This is the distinction that Chapter 17 (npm and supply chain) and Chapter 14 (Vue/Solid/Svelte) both rely on. A library is a small commitment, easily replaced. A runtime is a large commitment, hard to replace without rewriting most of an application. The MVC frameworks established the runtime pattern in the browser. The frameworks that followed (React, Vue, Angular 2+, the meta-frameworks) inherited it.
The trade-off is one of leverage. A good runtime lets a team work much faster than they could without it. A bad runtime, or a runtime whose design choices age badly, makes the team slower than they would’ve been writing platform code. The MVC era’s frameworks were good runtimes for their moment. The AngularJS-to-Angular transition is the warning about what happens when the moment changes.
What MVC Didn’t Solve
Section titled “What MVC Didn’t Solve”The browser MVC era improved structure. It didn’t solve every kind of modularity.
The frameworks helped organize UI — data, views, input, navigation. Many product capabilities still leaked through application code. Analytics calls. Error logging. Permission checks. Feature flags. Notifications. Storage. Audit. Experiments. Validation. Design-system rules. Each of these is a capability that wants to participate in the application’s flow without being tied to a specific view or controller. The MVC frameworks didn’t give them a clean home.
A controller still became a dumping ground for whatever didn’t fit elsewhere. A component still imported a dozen services. A route still triggered side effects in multiple unrelated systems. A form still knew about authentication, validation, audit logging, and the third-party analytics SDK.
This is the gap the rest of the book keeps circling. UI modularity and product-capability modularity are different problems. The MVC frameworks solved the first one and left the second one to the application developer. Twenty years later, the second one is still mostly unsolved in mainstream frameworks. Kitsune’s modules and capabilities (introduced in Part III) are an attempt at the second problem.
What Comes Next
Section titled “What Comes Next”This chapter has been about the moment frontend became application development. The next chapter takes a brief detour into the mobile-web shift that ran alongside the framework era. After that, the chronology continues with React’s 2013 debut and the rendering-from-state model that replaced MVC’s two-way binding as the dominant approach to client-side UI.
Exercise: Build a Tiny MVC Todo App
Section titled “Exercise: Build a Tiny MVC Todo App”Build a small todo app in plain JavaScript using explicit MVC-like separation.
The requirements: add a todo, toggle a todo’s complete state, delete a todo, filter all/active/completed, show a count.
Structure the code as three pieces:
const model = { todos: [], listeners: [], addTodo(text) { /* ... */ }, toggleTodo(id) { /* ... */ }, deleteTodo(id) { /* ... */ }, subscribe(listener) { /* ... */ }}
function renderView(state) { // return or update DOM based on state}
const controller = { addTodoFromForm(event) { /* ... */ }, toggleTodoFromClick(id) { /* ... */ }, changeFilter(filter) { /* ... */ }}Reflection:
- What felt cleaner after separating model, view, and controller?
- Where did the separation feel artificial?
- Where did the URL state belong — in the model, the controller, somewhere else?
- Where would you put analytics? Storage? Validation? Permissions?
- If you had to add the same set of features to three different applications, which parts of this architecture would you reuse and which would you rewrite?
The point is to feel both the value and the limits of application structure organized only around model, view, and controller. The frameworks in this chapter went further; the chapters that follow show where they went, and what they couldn’t solve.