In Chapter 6, I improved the layering of a shopping list application by separating the HTML generation code (the presentation layer) from domain and persistence logic. The final version separates layers into modules. However, the server is responsible for executing all three layers.
A consequence of this architecture is that every user interaction involves a full HTTP page request. This architecture limits how responsive the application will feel to end-users.
JavaScript can improve user experience. Presentation logic can be implemented in JavaScript and executed in the client’s web browser. Because this execution happens in the user’s browser, the web page can update without waiting for a network request to complete.
At first, JavaScript code could not directly communicate with any server. This limitation restricted JavaScript to calculators, animations, charts and tables that require no additional data during interactions. This limitation disappeared in the late 1990s when Microsoft’s Internet Explorer first introduced XMLHttpRequest (XHR). This feature enables JavaScript to make separate requests to the server, without a full page reload.
This idea gradually evolved to support the concept of single-page applications, where all presentation logic runs in the web browser. The server only provides a public API. Presentation logic runs in the user’s browser, where it can respond immediately, as opposed to the browser performing a complete HTML page request with every interaction.
Single-page applications are a dramatic architectural change. They vastly simplify server-side development and make it easier to create a reusable public API.
React and Angular are two popular frameworks for building single-page applications. They make it easier to write JavaScript that runs in the browser to generate and manipulate HTML.
An API
In a single-page application, the web server does not generate HTML directly. Instead, presentation logic running in the browser performs HTTP requests to an API running on the server.
While HTML is well suited for describing documents, it is not ideal for sending data for further client-side processing. As a result, XML and JSON are preferred over HTML.
XML is markup language similar to HTML, but has more strict rules for correctness. [1] XML might represent a shopping list as follows:
<?xml version="1.0" ?>
<list>
<item>
<description>Chocolate bar</description>
<quantity>5</quantity>
</item>
<item>
<description>Pasta</description>
<quantity>1</quantity>
</item>
</list>
JSON is a simple markup language based on the JavaScript language. [2] As JSON data, the shopping list may look like this:
[
{ "description": "Chocolate bar", "quantity": 5 },
{ "description": "Pasta", "quantity": 1 }
]
Using the res.json(…)
method in Express.js, it is easy to create an API that generates JSON. For example, consider this simple server:
const express = require('express');
const app = express();
const port = 8080;
let data = [
{ "description": "Chocolate bar", "quantity": 5 },
{ "description": "Pasta", "quantity": 1 }
];
// API endpoint for retrieving items
app.get('/api/items', (req, res) => res.json(data));
app.listen(8080, () => console.log(`API running at http://localhost:${port}/api/items`));
If you run the server and open http://localhost:8080/api/items
in your browser, you will see the data served in JSON format.
Communicating with the server API
There are three main ways to communicate with the server API from the presentation logic written in JavaScript and running in the browser:
- XMLHttpRequest (XHR)
-
XMLHttpRequest is the original technology from the late 1990s. It is not an easy-to-use or well-designed API, so its use is discouraged. [3] You can find examples on the Mozilla Developer Network.
- Fetch
-
The modern Fetch API was introduced to browsers in 2015, inspired by the design of the
.ajax(…)
function in the popular jQuery library. The API is clear and straightforward to use. You can see examples on the Mozilla Developer Network. - Polyfills and libraries
-
There are popular libraries, such as axios, superagent, unfetch, cross-fetch and jQuery.ajax, that provide high-level alternatives to XHR and fetch. These libraries internally make use of XHR and fetch, but provide a consistent interface that maintains compatibility with old browsers. In Angular, you can also use @angular/common/http.
The following code is a simple single-page application that uses fetch
to retrieve JSON data from the API.
<!DOCTYPE html>
<title>JSON fetch example</title>
<h1>JSON fetch example</h1>
<p id="server_status">Waiting for server...</p>
<script>
// Helper for setting the status
function setStatus(message) {
server_status.innerText = message;
}
// ---------------------------------
// Fetch without error handling
// ---------------------------------
/*
fetch('/api/items')
.then(response => response.json())
.then(as_json => setStatus(`${as_json.length} items received`));
*/
// ---------------------------------
// Fetch with error handling
// ---------------------------------
fetch('/api/items')
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
})
.then(as_json => setStatus(`${as_json.length} items received`))
.catch(error => setStatus(`An error occurred: ${error}`));
</script>
Warning
|
The example above uses server_status.innerText for simplicity. When using frameworks such as React or Angular to develop a SPA, there are better approaches to binding values to text. The innerText and innerHTML properties have performance and security issues. Your code is likely to have design issues if you are using innerText in React or Angular.
|
A shopping list API
Moving the presentation logic into the client simplifies the server-side Node.js code of the shopping. The server is no longer responsible for dynamically generating HTML. For example, compare the complete server-side logic of the shopping list application with the shopping list application in Chapter 6.
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
// Enable the JSON body parser so that Express.js will decode JSON in POST messages
app.use(bodyParser.json());
// Underlying data store, an array of { description: string, quantity: number }
let itemData = [];
// Retrieve all items in the shopping list
// returns an array of { description: string, quantity: number }
function findAllItems() {
return itemData;
}
// Insert a single item into the shopping list
// description is a string, quantity is an integer
// the parameters are assumed to be valid (non-null, non-empty)
function insertItem(description, quantity) {
itemData.push({ description, quantity });
}
// Compute the total quantity of items
function computeTotalQuantity() {
let total = 0;
for (let item of findAllItems()) {
total += item.quantity;
}
return total;
}
// Return the single-page web application
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
// Provide a list of all shopping list items and a total
// returns a JSON object: { items, total }
// where items is an array of { description: string, quantity: number}
// and total is a number (the sum of quantity)
app.get('/api/items', (req, res) => {
res.json({
items: findAllItems(),
total: computeTotalQuantity()
});
});
// Create a new shopping list item
// requires { description: string, quantity: number}
// returns a JSON object: { success: true }, if successful
app.post('/api/items', (req, res) => {
insertItem(req.body.description, req.body.quantity);
res.json({ success: true });
});
app.listen(port, () => console.log(The shopping list is running on http://localhost:${port}/
));
The workbook git repository includes a fetch-based single-page application written using plain JavaScript (chapter07_spa
). However, please note that the fetch code would be more correctly embedded in a React application or rewritten to use @angular/common/http
in Angular.
Tip
|
Moving the persistence and domain logic into separate modules will further improve the layering of the server-side code. I have included an example in the workbook git repository (chapter07_spa_layers ).
|