Skip to main content

Intro to Single-Page Applications (SPAs) and the MERN-Stack

Learning Objectives​

Students Will Be Able To:
Explain the difference between a SPA and a traditional multi-page web application
Identify the three development concepts that make SPAs possible
Structure a MERN-Stack Project

Road Map​

  1. Intro to SPAs
  2. Intro to the MERN-Stack
  3. This Unit's Reference App: SEI CAFE
  4. Building a MERN-Stack's Infrastructure
  5. Setting Up the mern-infrastructure Project
  6. Configure React for Full-stack Development
  7. Essential Questions
  8. Further Study

Videos​

Video πŸ“Ή Link

1. Intro to SPAs​

Review - What is a Single-Page App?​

We've mentioned them previously - what are they?

In a traditional multi-page web app, if we click a link, submit data via a form, or type in the address bar and press [enter], what happens?

In a SPA, we still want to be able to access different functionality by clicking links, submitting data to the server, etc., however, we want the UI to update without triggering a full-page refresh.

There are three development concepts that make SPAs possible:

  1. AJAX Communications between client and server
  2. Client-Side Routing
  3. Client-Side Rendering

Concept 1: Client/Server Communication via AJAX​

As you've seen, the fetch API, as well as utilities such as Axios & jQuery's AJAX methods can be used to send HTTP requests to a server using JavaScript instead of using forms and links in the page.

With AJAX, the server responds to an HTTP request with an HTTP response that usually contains a JSON payload in the response body.

Because the request was sent via code, and the response handled via code, the browser won't reload the page!

Concept 2: Client-Side Routing​

When the users have interacted by clicking links and submitting forms in the traditional multi-page web apps we've built thus far, the server has responded with a new HTML document that the browser proceeds to replace the current page with.

In a SPA, we still need a way to switch to different "pages" of functionality (see diagram above) - but without replacing the entire HTML document that's currently loaded in the browser...

Client-side routing is what enables users of a SPA to navigate to different "pages" without triggering a full-page refresh.

The users will still be clicking navigation "links" that cause the browser's address bar to change.

However, the client-side router intercepts the changes to the path by tapping into the browser's History API.

By using the History API, the client-side router can manipulate the path in the address bar without triggering an HTTP request.

note

πŸ‘€ We will continue to define server-side routes, however, the vast majority of those routes will be API-type routes that are accessed via AJAX calls, perform CRUD and return data as JSON to the browser.

Feel free to checkout the Further Study section to learn more about the History API and Hash URIs.

Concept 3: Client-Side Rendering​

So, assume a user clicks an Add Comment button in a SPA and expects to see the new comment show up in the list of comments...

This is a SPA, so you don't want the button to cause a full-page refresh!

In SPAs, you would send an AJAX request containing the data for the new comment to the server.

The server would update the database respond with JSON data - probably containing the current comments in an array.

However, to make the comment show up in the UI, it needs to be updated using JavaScript - a process we call client-side rendering.

Guess who the undisputed client-side rendering champion is...


The React Library - of course!


❓ Review Questions - SPAs (1 min)​

(1) What's the most significant difference between a traditional multi-page web app and a single-page app?


A SPA avoids full-page reloads.

A SPA loads the index.html once and then has its DOM modified via client-side rendering.


(2) What three development concepts enable the creation of comprehensive single-page applications?


  • AJAX Communications between client and server
  • Client-Side Routing
  • Client-Side Rendering

2. Intro to the MERN-Stack​

A technology stack, also called a solutions stack, is a group of development tools/services used to build an application.

For example, the LAMP-Stack is a mature technology stack that uses:

  • Linux
  • Apache (web server)
  • MySQL and
  • PHP (web development programming language)

In the next unit, our stack will consist of:

  • Python
  • Django and
  • PostgreSQL

There are a few tech stacks that use MongoDB, Express & Node as their backend solutions.

Then, one of the following front-end technologies is added to go full-stack:

  • React which results in the MERN-Stack
  • Angular which results in the MEAN-Stack
  • Vue.js which results in the MEVN-Stack

The MERN-Stack is by far the most popular tech stack currently and will remain so into the foreseeable future.

Architecture of the MERN-Stack​

The following depicts the overall architecture of a MERN-Stack app:

The flow is as follows:

  1. When the user browses to the app's URL, the Express server always delivers the static public/index.html page.
note

πŸ‘€ There will be no EJS templates on the server - just the static index.html. In fact, there's no reason to install the EJS template engine!

  1. When the browser loads index.html, it will request the JS that is the React app (outlined in blue).

  2. The code in the React app's index.js module runs, which causes the React app to render for the first time. During this initial rendering, the client-side routing library renders components based upon the URL in the address bar.

  3. After the React app has been loaded, all subsequent HTTP communications between the client and server will be via AJAX in order to avoid the page from being reloaded.

  4. Certain components may want to CRUD data on the server. However, we won't litter components with the code responsible for CRUD. Instead, as a best practice, that code will be organized into service/API modules.

  5. On the server, a single non-API "traditional" route will be defined with the purpose of delivering the static index.html file. We will refer to this route as the "catch all" route since it will match all GET requests that do not match any of the "API" routes...

  6. Other than the single "catch all" route just mentioned, all other routes on the server will be defined to respond to AJAX requests with JSON. By convention, the endpoints of these routes with be prefaced with /api, e.g., /api/cats, /api/login, etc.

Now that we know a bit about the MERN-Stack, let's take a look at the reference app we'll build together this unit...

3. This Unit's Reference App: SEI CAFE​

As you know, it's important to practice the individual skills we learn for a given technology by bringing them together to build a real-world working application.

This unit's reference app is a MERN-Stack online food ordering app called SEI CAFE.

Be sure to sign-up:

and place an orders!

4. Building a MERN-Stack's Infrastructure​

We'll begin by building out the infrastructure (boilerplate) that most real-world MERN-Stack apps starts with, including client-side routing and authentication.

After the infrastructure code is complete, we'll save the project to a separate repo that can be cloned to launch future MERN-Stack projects, including your capstone project at the end of this unit!

How to Structure a MERN-Stack Project​

Up until this point, we've taken for granted that full-stack apps, like your Express and Django projects, were single, integrated projects.

However, developing a MERN-stack project involves complexities such as tooling such as webpack, React's development server, etc.

Additionally, there are concerns during both development and production that have to be addressed...

During Development​

A React project uses a development server that compiles and serves the React app to the browser at localhost:3000.

❓ There's a conflict between React's development server and the Express applications we've built previously - what is it?


They both run on port 3000 by default and only a single process can run on a given port.


Luckily, the React team recognized this conflict and has a solution which we'll see in a bit.

Production Environment Concerns​

As we develop our React app locally, we're writing source code that React's dev server builds and runs automatically. However, this is not production ready code because it has extra debugging logic, etc.

In a moment will see how to build the React app locally.

However, this locally built code is typically git ignored thus it's important to ensure that whatever hosting service you deploy to is configured to build the React app in the cloud each time the code is deployed.

Luckily for us, since 2019, Heroku automatically builds the React apps each time they are deployed.

In addition to ensuring that the hosting service builds the React app, we will also need to code the Express app to serve the built production code.

Possible MERN-Stack Project Structures​

There are two general architectures we could pursue:

  1. Maintain two separate projects, one for the React app, the other for the Express backend.
  2. Integrate the codebase for both the React frontend and the Express backend.
ArchitectureProsCons
Separate Projects
  • Better for when the backend services multiple frontend projects (web, native mobile, desktop).
  • Must manage two projects and git repos.
  • Must deploy those two projects separately.
  • The React project will require code and/or configuration to access the correct backend during development (localhost) and production (could be anywhere).
  • Must implement CORS.
Single Project
  • A single integrated project.
  • None of the above Cons.
  • Not the best project structure to re-use the same backend project to service multiple frontend projects, e.g., Web/Mobile/Desktop

The single, integrated project approach looks to be a no-brainer. But, what does the structure of a single project look like?

Again, two options:

  1. Start with an Express app, then generate the React app within it (naming it client or something similar). This approach will result in nested package.json files and node_modules folders requiring you to "know where you are" when installing additional Node modules. Also with this approach, Heroku will not "see" the React app and will not build it automatically.
  2. Start with a React app, then add an Express server.js and other server related folders/files as necessary. This approach results in a single package.json file and node_modules folder.

The second option is "cleaner" and less error prone, so we'll opt for that approach.

Let's start building!

5. Setting Up the mern-infrastructure Project​

Here's the plan:

  • Generate the React app
  • Build the React app's production code
  • Code the skeleton Express app
  • Define the "catch all" route in the Express backend
  • Test the Express server

Let's do this!

Generate the React App​

The best way to create a React project is by using the create-react-app script provided by the React team:

cd ~/code
npx create-react-app mern-infrastructure
tip

πŸ‘€ A new folder will be created named mern-infrastructure. If you would like to generate a project in the future within an existing folder, you can use . in place of the project name.

Creating a new React app takes some time because create-react-app automatically installs the Node modules - and there's a ton of them!

info

πŸ‘€ It's best to ignore all severity vulnerabilities. There's been much written about how create-react-app is simply tooling and that we should ignore npm's overreactions.

Let's briefly review the outputted message:

Created git commit.

Success! Created mern-infrastructure at /Users/<your username>/code/mern-infrastructure
Inside that directory, you can run several commands:

npm start
Starts the development server.

npm run build
Bundles the app into static files for production.

npm test
Starts the test runner.

npm run eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

cd mern-infrastructure
npm start

Happy hacking!

Now we can:

cd mern-infrastructure

Open the project in VS Code:

code .

Open an integrated terminal in VS Code:

control + backtick

CRA has already initialized a repo, so let's connect to the code-along repo that you can sync with it as needed:

git remote add origin https://git.generalassemb.ly/sei-blended-learning/mern-infrastructure.git
git fetch --all

Spin up React's built-in development server:

npm start

which automatically opens the app in a browser tab at localhost:3000:

The React development server automatically builds and reloads the app in the browser whenever changes are saved.

Within VS Code, we'll find a Node project's usual package.json, node_modules, etc.

The React project's source code lives within the src folder:

Build the React App's Production Code​

We will soon be coding the Express server to serve the production React app.

Thus, we need to build the React app's code locally into production code at least once so that the Express server does not raise an error.

The create-react-app CLI includes tooling and a build script in package.json that, when run, compiles the the code in the src and public folders of the React project into production code - placing it into a folder named build.

Let's run the build script:

npm run build
caution

πŸ‘€ npm requires us to use the run command for scripts other than start and test.

After building, examining our project's structure reveals a new build folder containing production ready static assets including index.html, static/css & static/js folders, etc. If this React app was a frontend only app, the assets in the build folder would be ready to deploy to any static hosting service.

This build folder of production-ready goodness is ready to be served up by an Express backend...

Code the Skeleton Express App​

Now with the React app up and running, we can start to code the Express backend.

We could use Express generator if we save the existing React-oriented package.json file and merge it with the Express dependencies.

Instead we're going to code our own Express app from scratch because we won't need much middleware, etc. due to the fact that the Express backend simply needs to:

  • Deliver the production-ready index.html, which will in turn request the production-ready scripts, etc.
  • Respond to AJAX requests, performing any necessary CRUD, and finally respond with JSON.

Install the Modules for the Express Server​

There's no problem with the Express project happily sharing that same package.json that create-react-app created.

For now, we're only going to install a minimal number of modules for the Express app:

npm i express morgan serve-favicon

Again, we don't need a view engine because our server will be either serving static assets (index.html, CSS, JS, images, etc.) or responding to AJAX requests with JSON. There will be no EJS templates!

Later, we'll install additional modules, e.g., mongoose, dotenv, etc.

Create and Code the Express App (server.js)​

Let's code our Express server:

  1. Ensure that you're still in the root folder of the React project.

  2. touch server.js

  3. At the top of server.js, let's do all the familiar stuff::

server.js
const express = require("express");
const path = require("path");
const favicon = require("serve-favicon");
const logger = require("morgan");

const app = express();

app.use(logger("dev"));
app.use(express.json());

❓ Why don't we need to mount the express.urlencoded() middleware also?


Because express.urlencoded middleware is used to process data submitted by a form - and we don't submit forms in a SPA.


  1. Mount and configure the serve-favicon & static middleware so that they serve from the build (production) folder:
server.js
app.use(express.json());

// Configure both serve-favicon & static middleware
// to serve from the production 'build' folder
app.use(favicon(path.join(__dirname, "build", "favicon.ico")));
app.use(express.static(path.join(__dirname, "build")));
  1. Set the port for development to use 3001 so that React's dev server can continue to use 3000 and finally, tell the Express app to listen for incoming requests:
server.js
// Configure to use port 3001 instead of 3000 during
// development to avoid collision with React's dev server
const port = process.env.PORT || 3001;

app.listen(port, function () {
console.log(`Express app running on port ${port}`);
});

Define the "Catch All" Route​

A single "catch all" route is required to serve the index.html when any non-AJAX "API" request is received by the Express app:

server.js
app.use(express.static(path.join(__dirname, "build")));

// Put API routes here, before the "catch all" route

// The following "catch all" route (note the *) is necessary
// to return the index.html on all non-AJAX requests
app.get("/*", function (req, res) {
res.sendFile(path.join(__dirname, "build", "index.html"));
});
caution

πŸ‘€ Since this route is a "catch all" that matches every GET request, be sure to mount API or other routes before it!

Now the "catch all" route will serve the index.html whenever:

  • A user types a path into the address bar and presses enter.
  • The user refreshes the browser.
  • A person clicks a link to the app provided via email, slacked, included on another web page, etc.

For example, let's say you provide a friend with a link to your app's dashboard functionality like the following:

https://amazingapp.com/dashboard

When clicked, the server will receive GET /dashboard request, but won't that be a problem because the /dashboard path is meant to be a client-side route?

Nope, no problem at all because the "catch-all" route will send the index.html page to the browser.

Then, after index.html loads in the browser, the React app's client-side routing will render components based upon the /dashboard path in the address bar - and all is well!

Test the Express Server​

We should now be able to test the Express server.

However, we can no longer just type nodemon because just typing nodemon will run the command in the start script in package.json and that script is being used to start the React development server instead.

Therefore, in our MERN-Stack development environment, it's important to start the Express server by typing:

nodemon server

As expected, the Express server will run on port 3001 instead of 3000 (which is where the React dev server runs).

Browsing to localhost:3001 will display the built production React app!

❓ What command must be run to update the React app's production code?


npm run build


6. Configure React for MERN-Stack Development​

Note how we're viewing the React app without the React development server running, again, this is because we are viewing the production code that's in the build folder, not the code as it exists in the src folder.

So, when you are developing and nothing seems to be updating in the browser - be sure to verify that you are browsing at localhost:3000!

Running Both Express & React During Development​

To develop a MERN-Stack app, you'll need two separate Terminal sessions for running:

  1. The Express backend (which is better to start first)

    ❓ If we don't already have the Express server running, we start it with what command?


    nodemon server

  2. React's development server

    ❓ What's the command to start React's dev server in the second Terminal?

    npm start

Now, browse to localhost:3000, not 3001!

So far, so good, just one more configuration issue...

Ensuring that the React Dev Server Sends AJAX Calls to the Express Server​

Let's think ahead to when we begin to make AJAX requests from the React app to our server using code like this:

return fetch("/api/orders/history").then((res) => res.json());

❓ Which host/server will that fetch request be sent to?


The same host as shown in the address bar:
localhost:3000


❓ Where do we need the fetch requests be sent to during development?


The Express server that's listening for AJAX requests at:
localhost:3001 !


Luckily, the React team has created an easy fix for this dilemma. The React development server allows us to configure a "proxy" which specifies the host to forward API/AJAX calls to.

The fix is to add a "proxy" property in the package.json (be sure that it's a "top-level" property):

package.json
  "browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:3001"
}
note

πŸ‘€ The React dev server will NOT automatically restart when changes are made to the package.json file.

Now the React development server will forward all AJAX calls, such as fetch('/api/todos'), to localhost:3001 instead of localhost:3000.

BTW, this is only an issue during development - the deployed app will be just fine thanks to the way we structured the app as a single project.

Welcome to the MERN-Stack​

πŸ‘€ Do you need to sync your code?


git reset --hard origin/sync-1-finish-intro


7. ❓ Essential Questions (2 mins)​

(1) What folder holds a React app's production-ready code?


The build folder


(2) What's the responsibility of the "catch all" route defined in the Express app?


Send back the index.html page for all non-API/AJAX requests.


(3) True or False: API routes will need to be defined in the Express app so that the React app can CRUD data, etc. on the server.


True


(4) True or False: The React app should use a "service/api" module to communicate with the backend's API routes via AJAX.


True. We don't want to "litter" the React components with a bunch of AJAX code.


8. Further Study​

HTML5's History API​

Using HTML5's History API, an application in the browser is able to manipulate the browser's current URL using JS and without triggering a server request.

Client-side router software can use the History API to perform client-side routing to load different "screens" of functionality and perform other magic without a causing a request to be sent to the server, thus there's no full-page refresh.

This approach works wonderfully when the client router is in charge and is the only thing manipulating the URL in the address bar. However, what about when a user enters a URL manually, or a link external to the client app is clicked? These cases require a small bit of configuration on the server - a simple "catch all" route that handles all requests that don't match requests for static assets, API routes, etc. The catch all route will then return the index.html and all is well.

Later in this unit you'll be introduced to the popular React Router, which uses the History API to perform client-side routing in React SPAs.

Browser Hash Navigation​

The HTML specification includes what is known as Hash URIs.

Hash URIs include a "hash" (#) in the URI, for example:
https://facebook.github.io/react/docs/react-dom.html#reference

If we browse to the above link, we will see that it takes us directly to the "Reference" section on the page.

Hovering over other titles/sub-titles on the page reveals other links that have their href's set to a value prefaced with a "#", for example:

<a class="hash-link" href="#unmountcomponentatnode">#</a>

Notice that when we can click on these links, the address bar changes, but the browser does not make an HTTP request.

Today's client-side routers lean toward using the History API over Hash URIs due mainly to the fact that the URL's are "prettier" without the hash.

References​

React Docs