In the classic web browser UI experience, the user navigates a web of HTML documents. The user starts at a particular document whose URL is well-known or book-marked, or discovered by search. From there, the user clicks on links. Those links lead to other documents that will be loaded in the browser. The URL for the current document is always displayed in the address bar of the browser, and it can be book-marked or sent in an email to someone else. The user can also navigate back and forward in the browser history of these URLs.
This model is extraordinarily powerful and simple, and has been captured in a formal description called REST. This model is still the foundation for most of the theory and a large part of the practice of the web.
Early single-page applications had the unfortunate problem that they typically were incompatible with the "classic web", and lost many of the benefits of the classic web. Two of the primary problems were that they did not integrate well with browser history and they did not integrate well with Search-engine optimization. Single-page applications have got better at this, but many of them still have problems. The problems typically revolve around the definition of URLs and their representations. In our team, we believe we have discovered a particularly simple and elegant approach to writing single-page applications that has the benefit of being perfectly integrated with the classic web model.
Our model for single-page applications
Our model is really embarrassingly simple – it really just says "don’t stray from the basic principles of the classic world-wide web when implementing your single-page applications".
In the classic model of the world-wide web, the focus is on the documents and their representations. If your problem domain was internet shopping, and you were developing a "classic" web app, you would probably have documents for catalog items, orders, and customers. The following would be true:
- Each entity has a single URL
- URLs are straightforward
- Every entity has an HTML representation
- Each HTML representation includes all the data regarding this entity
- The representation of each entity includes URL links to other entities
- Other links might point outside the application, and might point to entities of different types
We believe that most of the problems that you see with single-page applications happen when people stray from these very simple principles. The reason people have strayed is either that they have lost sight of why these simple rules are important, or they have been unable to see how to implement their single-page applications without departing from them. We hope to show you that you don’t have to stray to implement single-page applications with all the benefits you want.
What not to do
Let us look in turn at the ways people have violated these principles.
Creating multiple URLs for the same conceptual entity
A common pattern is to make one URL for the HTML representation, which includes the single-page application implementation, and a separate URL for the data. Designers often also create an additional non-URL identifier that is used for other purposes, like query or command-line. Creating multiple URLs and non-URL identifiers like this creates considerable unnecessary complexity. Which one could/should I book-mark or email to a friend? If I have one and I need the other, how do I convert? Which one can I use in queries? How do I recognize if 2 identifiers actually reference the same entity?
The alternative is to use content-negotiation to return the representation that the client needs from a single URL.
A problem that people struggle to address in this model is the desire to allow a different group of people to control the HTML representation from the JSON representation – we will describe later how you can address this without departing from the simple web model.
Complicated, unnatural URLs
Early single-page applications used URLs with a fragment identifier (the part after the #) to implement page transitions. The primary reason for this was to allow the browser history to be adjusted correctly without triggering a browser document load. The introduction of a history API in HTML5 (there are also compatibility libraries for older browsers) has eliminated the need for this, allowing developers to return to 'natural' URLs for all entities.
Entities with no HTML representation
Some single-page application implementations omit the HTML representation of entities completely. Since the UI implementation loads JSON data, implementers see no need to implement an HTML representation for each entity – a single HTML representation for the SPA itself is enough. This creates many problems – there is nothing for web bots like search engines to crawl, and the individual entities cannot be usefully book-marked.
Omit the data from the HTML
One approach that people have taken with single-page applications is to include only the single-page application code in the HTML representation. When the single-page application loads and begins to execute, it will make a second call to the server to retrieve the data for the resource as JSON. This approach introduces multiple problems and should be avoided. Some of the problems are:
- It requires an extra trip to the server to load a page.
This approach does have a nice property, though – it creates a clean separation of the single-page application implementation from the data of individual resources. We will describe later a different way to achieve that separation without departing from the fundamental, simple web model.
No links in representations
When people design HTML representations, they typically include HTML links to other entities – we all understand this is how HTML works. When people design JSON representations, they commonly omit all links. Instead, they may include non-URL identifiers, and additionally publish application-specific rules to allow people to manufacture URLs from the non-URL identifiers. This is very unfortunate – links are very valuable in JSON for the same reasons they are valuable in HTML. Specifically, it allows software to be written that can navigate and analyze the web of information without requiring application-specific knowledge.
One of the primary characteristics of the world-wide web that made it powerful is that it supports linking across sites. Not only that, it allows linking to resources of different types – for example, an HTML resource can point to a jpeg movie, a gif image, or even a PDF or Word document. Many single-page applications are coded with closed-world assumptions – the implementation assumes it knows exactly the type of entity each link must point to. This is a big lost opportunity. Not only does it prevent that SPA from participating in the broader WWW, it even causes problems with mundane technical issues like version evolution.
What to do instead
Define a single set of URLs with content negotiation
For each conceptual entity – e.g. order, catalog item, customer – define a single URL. Return both HTML and JSON representations of the same entity at the same URL.
Make your URLs simple and obvious
Avoid fragment identifiers.
Include URL links in your JSON as well as in your HTML
Hypertext is very powerful for APIs as well as UI
Code your clients to tolerate links to things they doesn’t understand
Participate in the broader world-wide web. That thing you can't understand may be a future version of something you previously understood.
Separating UI implementation from business logic implementation
A common objection to the sort of content-negotiation we are advocating is that people do not see immediately how it is compatible with modern single-page applications and the frameworks that support them. If you are a Ruby on Rails programmer, you are probably already very familiar with the idea of providing both HTML and JSON representations of the same entities from the same URLs. If your implementation follows the design pattern of Rails, you can use normal programming–language modularization techniques to allow different teams to worry about different representations and the business logic. If, like many of us, you prefer to move your UI implementation out of the server, you need a different approach.
Following the principles above, we know that the HTML representation of the entity has to include both the data for the state of the entity, plus the UI implementation, otherwise we violate basic web architecture. Most of the HTML that was returned by servers in the 1990s and early 2000's inter-mingled the UI implementation with the data, something like this:
The <form> element encodes the UI, while "firstname", "Mickey" and "lastname", "Mouse" are the data. Mingling the data and the UI implementation like this makes it difficult to support independent teams.
The approach we have used successfully to solve this problem is to change the HTML returned by the server to look like this:
/spa.js can then use the data from the HTML DOM along with the template to produce new HTML that implements the desired UI. This HTML can then be inserted in the DOM to replace or augment the HTML returned by the server. This is a mustache template, but you can use any technology you like including Angular or Knockout.js to do similar things, or just do your own thing. It is important to note that neither /spa.js nor the template needs to reside on the same server that served up the original HTML that held the data. This makes it very convenient to separate the code of the UI team from the code of the server team. The server team is responsible for the JSON API and also for the HTML that will be seen by HTML web bots like search engines. The UI team is responsible for user interface, and, since the UI implementation files are static, they can even be served from CDN, which gives some interesting options for performance tuning. If you are not worried about search-engine optimization, the HTML rendered by the server can be calculated algorithmically directly from the JSON, so the server team can focus only on the JSON.
We use the same /spa.js for all the resources that are handled by the same single-page application. Since the same /spa.js handles resources of different types, the first thing that /spa.js will do on load is look at the type of resource in order to figure out how to render it in the UI. The simplest way to do this is to make sure that every resource has a "type" property. It is obvious that /spa.js will have to do this on the original HTML document load, but in fact we do exactly the same thing whenever /spa.js loads a JSON representation programmatically. This allows /spa.js to be resilient if the entity that was loaded was not what it expected. For example if the response content-type is unexpected (it's a gif), or the type in the data is unexpected (not an entity that /spa.js can handle) or the version is unexpected (a future incompatible version), /spa.js can recover simply and elegantly by forcing a full document load of the entity and letting the browser sort it out.
It is often desirable to create resources that are specific to a particular UI. Suppose one UI view wants to show orders that were returned by customers with poor credit grouped by geographic region. One approach might be to implement a general-purpose query API that is not specific to this particular UI, but which can answer queries like this. If you can do this, it’s a nice strategy, but often you just need to do something specific for one UI. It is perfectly permissible in this model to create a UI-specific server that implements a new resource whose meaning is exactly "orders that were returned by customers with poor credit grouped by geographic region". Just don’t use the UI server to implement its own parallel set of URLs that correspond to existing entities exposed by the logic server, and to show those parallel URLs to users.
This model does not constrain in any way how you implement your UI, and neither does it constrain how you implement your servers. The only way it might change the way you are accustomed to writing a single-page application is in how it bootstraps itself on the original browser document load. What you get in return is perfect integration of your SPA with the rest of the WWW.
 "single-document applications" and "multi-resource applications" would have been better names. Their distinguishing characteristic is that they display multiple consecutive web resources from a single initial browser document load.
 Technically speaking the fragment identifier is not part of the URL (or URI) at all – the URL (URI) includes only the part that flows from the client to the server. The combination of the URL plus the fragment identifier is called a "URI reference". What this means is that in these designs, every entity in the system actually has the same URL, and so they cannot be addressed individually in HTTP requests.