Infrastructure for Production Grade Single-Page Applications (SPAs) with 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
- Intro to SPAs
- Intro to the MERN-Stack
- This Unit's Reference App: SEI CAFE
- Building a MERN-Stack's Infrastructure
- Let's Begin Building mern-infrastructure
- Configure React for Full-stack Development
- Further Study
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:
- Client-Side Rendering
- Client-Side Routing
- AJAX Communications between client and server
Concept 1: 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 and send back a response (probably containing the updated list of comments).
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!
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. Intercepting the change to the path in the address bar prevents a request from ever being sent to the server. Instead, the SPA will react locally and this is why it's called client-side routing.
Note that 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 needed by the frontend.
Concept 3: 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 has no reason to reload the page!
❓ Review Questions - SPAs
- What's the most significant difference between a traditional multi-page web app and a single-page app?
- What three development concepts enable the creation of comprehensive single-page applications?
Intro to the MERN-Stack
The MERN-Stack is by far the most popular tech stack currently and will remain so into the foreseeable future.
MERN stands for MongoDB, Express.js, React, and Node.js. It is a comprehensive technology stack that provides developers with an end-to-end framework to build robust web/mobile applications. MongoDB is a NoSQL database used to store application data; Express.js is a backend web application framework used to create APIs and route HTTP requests; React is an open-source JavaScript library for building user interfaces; and Node.js is a JavaScript runtime environment that allows developers to run server-side scripts outside of the browser context. Together, these technologies provide frontend and backend development services, allowing developers to create full-stack applications with ease.
Architecture of the MERN-Stack
The following depicts the overall architecture of a MERN-Stack app:

The flow is as follows:
-
When the user browses to the app's URL, the Express server always delivers the static public/index.html page.
Note that there are no JSX templates on the server - just the static index.html. In fact, there's no reason to install the JSX template engine.
- When the browser loads index.html, it will request the scripts that contain the React app - this is shown as the blue CLIENT/React app.
- 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 path of the URL.
- After the index.html 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.
- 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.
- 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
GETrequests that do not match any of the "API" routes... - 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.
How to Structure a MERN-Stack Project
Up until this point, we've taken for granted that full-stack apps, like your Express and projects, were single, integrated projects.
However, developing a MERN-stack project involves complexities such as tooling, React's integrated development server, etc.
Additionally, there are concerns in both development and production environments 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.
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 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.
Luckily for us, beginning in 2019, Heroku started to automatically build React apps when 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:
- Maintain two separate projects, one for the React app, the other for the Express backend.
- Integrate the codebase for both the React frontend and the Express backend.
| Architecture | Pros | Cons |
|---|---|---|
| Separate Projects |
|
|
| Single Project |
|
|
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:
- Start with an Express app, then generate the React app within it (naming it
clientor 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. - 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.
You will see people do it both ways. Don't be one of them though lol.
Now that we've discussed how to structure a MERN-Stack app, let's take a look at the reference app we'll build together this unit...
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 a couple of orders!
Building a MERN-Stack's Infrastructure
We'll begin by building out the infrastructure (boilerplate) that every real-world MERN-Stack app 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 course!
Let's Begin Building mern-infrastructure
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!
1. 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-infrastructureNote: 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 also installs the Node modules - and there's a ton of them!
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 -
Spin up React's built-in development server:
npm start, which automatically opens the app in a browser tab atlocalhost: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:
2. Building the React App's Production Code
We will soon be coding the Express server to serve the production index.html page. 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 buildNote: npm requires us to use the
runcommand for scripts other thanstartandtest.
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 be deployed to any static hosting service.
This build folder of production-ready goodness is ready to be served up by an Express backend...
3. 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-faviconAgain, 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:
- Ensure that you're still in the root folder of the React project.
touch server.js-
At the top of server.js, let's do all the familiar stuff:
requirethe modules; create the Express app; and mount themorganlogging middleware andexpress.json()middleware that processes JSON data sent in the AJAX request and adds it to thereq.body: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.urlencodedmiddleware is used to process data submitted by a form - and we don't submit forms in a SPA. We use State. -
Mount and configure the
serve-favicon&staticmiddleware so that they serve from the build (production) folder: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'))); -
Set the port for development to use
3001so that React's dev server can continue to use3000and finally, tell the Express app to listen for incoming requests:// 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}`) });
4. 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:
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'));
});Note: Since this route is a "catch all" that matches every
GETrequest, 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.
- An "external" link in an email, included on another web page, etc. to the MERN-Stack app is clicked.
For example, if we slack the following link to a friend: https://sei-cafe.herokuapp.com/orders/new. The friend clicks on it, initiating an HTTP request to our server.
However, the /orders/new part of the URL is supposed to be for the client router - not the server! But there it is, and the server has to deal with it...
The server deals with it by, thanks to the "catch all" route, sending back index.html - which is what we want.
After index.html loads in the browser, the React app's client-side routing will render components based upon the /orders/new path in the address bar.
5. Test the Express Server
We should now be able to test the Express server.
However, please note that we can no longer just type nodemon because just typing nodemon relies on the start script in package.json to know what to run and that script is being used to start the React dev server instead.
Therefore, in the MERN-Stack development environment, it's important to start the Express server using:
nodemon serverWe need to add that script to our package.json in dev
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 our React app!
❓ Review Questions - MERN-Stack Development
- Is the app we're currently viewing at
localhost:3001the "production" version of the React app or the version within thesrcfolder? - What command must be run to update the React app's production code?
Configure React for MERN-stack Development
Note how we're viewing the React app without the React development server running - as we just discussed, this is because we are viewing the built production code, not the code as it exists in the src folder.
IMPORTANT: During development, you don't want to browse to
localhost:3001! Instead, you want the browser to load the React app from React's dev server onlocalhost:3000. You should only browse tolocalhost:3001to check out how the production code will run when deployed, however, don't forget to build before doing so.
So, when you are hacking out code and nothing seems to be updating in the browser - be sure to verify that you are browsing localhost:3000!
Running Both Express & React During Development
To develop a MERN-Stack app, you'll need two separate terminal sessions for running:
- The Express backend
- React's development server
❓ If we don't already have the Express server running, we start it with what command?
npm run dev❓ Now let's open a second terminal session and start React's dev server using what command?
npm startNow, browse to localhost:3000 - not 3001!
So far, so good, but there's an problem lurking...
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 actually need the fetch requests be sent to during development?
Our Express server that's listening for AJAX requests at localhost:3001 !
BTW, this is only a problem during development - the deployed app will be just fine thanks to our chosen MERN-Stack structure that uses a single project for the frontend and backend.
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" key in the TOP-LEVEL of the 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 during development, the React SPA can make AJAX calls, such as fetch('/api/todos'), and the request will be "proxied" (forwarded) to localhost:3001 instead of localhost:3000.
Now update your package.json
- When you deploy your code most hosts will automatically try to run your app by running
npm start - Unfortunately for us that script runs the react dev server, not a production server
- We need to change the
startscript to read what we want to happen in production "start":"node server.js"- Then we need a new script for our react dev server
"react":"react-scripts start"
Welcome to the MERN-stack!
❓ Essential Questions
- What folder holds a React app's production-ready code?
- What's the responsibility of the "catch all" route defined in the Express app?
- 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 or False: The React app should use a "service/api" module to communicate with the backend's API routes via AJAX.