Building a secure single-page application (SPA) with React.js _ by Colin Kraczkowsky _ Level Up Coding
Building a secure single-page application (SPA) with React.js _ by Colin Kraczkowsky _ Level Up Coding
67 2
First, we create a new directory called backend and migrate all of our backend
code into it.
$ mkdir backend
Let’s make sure that our Node.js application still works after the migration by
switching into the new directory and starting the application.
$ cd backend/
$ node ./index.js
Back in the root directory of our project, we create a new directory called
frontend where we will add all of our frontend code.
$ mkdir frontend
Recall from my previous blog Building a RESTful API with Node.js that the
frontend of our Node.js application was the very basic index.html file. Later in
this section, we will enhance this file but for now let’s move it from our backend
directory to our frontend directory.
$ mv backend/index.html frontend/
$ cd frontend/
$ touch index.js
And we move the code describing the content that the browser to display from
our index.html file into our new index.js file.
ReactDOM.render(
<div>
<h1>Hello World!</h1>
<h2>Item List</h2>
<ul>
<li>Item 1</li>
</ul>
<h2>Add Item</h2>
<form>
<label for=”item”>Item: </label>
<input type=”text”></input>
<br></br>
<input type=”submit” value=”Submit”></input>
</form>
<a href=”/content”>Click here for content!</a>
</div>, document.getElementById(‘root’)
);
Going into the specifics of React.js is out of the scope of this blog, so I will just
summarize what this code is doing. We load the react and react-dom libraries
which give us access to JSX, a syntax extension to JavaScript, and the Virtual
Document Object Model (VDOM), a node tree of React components that matches
actions to state. ReactDOM.render() creates this VDOM in which we use JSX to
render a React element. We wrap the HTML from our old index.html file in a
div element because React can only render a single element at a time.
As you can see, this file loads two new libraries, react and react-dom , so we
need to install their respective packages which we do using a familiar tool: the
Node package manager (npm).
<!doctype html>
<html>
<head>
</head>
<body>
<div id=”root”>
</body>
</html>
Recall from the index.js file that we supplied ReactDOM.render() with a second
argument. This argument is the container in which we want our React elements
rendered. We defined this container as document.getElementById(‘root’) which
grabs a reference to the DOM and asks it to return the element with an id of
‘root’. In the index.html file we supply this element. In fact, this is the only
element that we will supply in the index.html file. All other elements will be
rendered to the VDOM within this container element. This is very different from
early web development where a website was a series of HTML documents linked
together each of which were their own DOM object. As we build out our React.js
application, you will notice that we manipulate the presentation of the content
all within this single DOM object.
and return React elements describing what should appear on the screen.
Components are just JavaScript files, so let’s add one to our project.
$ touch PublicPage.js
Like we did in the index.js file, we load the react library to get it in scope so
that we have access to JSX syntax for use in our component. We then create an
instance of the Component class provided by React and name it PublicPage , it is
this name that we will use to refer to this component throughout our React.js
application. The Component class has a number of lifecycle methods which we can
use to manipulate the component from construction to rendering. In our
PublicPage component we use the render method to define what will be
rendered to the DOM. As we did in the index.js file, we wrap the elements that
we want rendered into a single div element because React can only render a
single element at a time. Below is the resulting document when this component
is rendered.
<!doctype html>
<html>
<head>
</head>
<body>
<div id=”root”>
<div>
<h1>This is a public page, everyone has access to this page!
</h1>
</div>
</div>
</body>
</html>
As you can see, React has taken the contents of our PublicPage component and
injected it into the body of our index.html file. Each component that we add to
our React.js application will render to the browser in this way.
Implementing navigation and routing for our frontend with React Router
At this point, the content of our React.js application consists of an index.js file
containing our main content and a PublicPage.js file containing a component.
The challenge at hand is to build a mechanism to transition between this content
and any future content that we will add. This mechanism will need to provide
means to define routes, to maintain a list of routes, to return the content
available at the route to the caller, to locate and match URLs to routes, and to
remember the history of the user’s requested routes. While we could build this
mechanism ourselves, the React Router library is already available and includes
this functionality. The library offers several packages depending on the type of
application being built, but we install the react-router-dom package since we are
building a web application.
As the index.js file is the entry point to our application, let’s refactor this file to
use React Router.
...
import { BrowserRouter as Router, Switch, Route, Link } from “react-
router-dom”;
import PublicPage from ‘./Components/PublicPage’;
ReactDOM.render(
<BrowserRouter>
<div>
<ul>
<li>
<Link to=’/public’>Public</Link>
</li>
</ul>
<Switch>
<Route path=’/public’>
<PublicPage />
</Route>
</Switch>
</div>
</BrowserRouter>, document.getElementById(‘root’)
);
When the hyperlink is clicked, the URL is requested and it is the Switch
component that tells React Router which Route component to render depending
on the pathname. The Route component is then responsible for rendering the
appropriate component. In our case, we have placed our PublicPage component
at the pathname ‘/public’ so that any request to http://localhost:3000/public
should return that component.
$ touch LoginPage.js
function handleSubmit(event){
event.preventDefault();
axios.post(url, {email, password})
.then((res) => {
document.cookie = ‘my-token=’+res.data.token+’; max-age=60;’;
history.push(from);})
.catch((error) => {
console.log(error);});
};
function handleOnChange(event){
if (event.target.name === ‘email’){
setEmail(event.target.value);
};
if (event.target.name === ‘password’){
setPassword(event.target.value);
};
};
return(
<form onSubmit={handleSubmit}>
<h1>Login to your account:</h1>
<label>Email: </label>
<input onChange={handleOnChange} name=”email”
placeholder=”freddie.mercury@gmail.com” size=”35" value={email}/>
<br/>
<label>Password: </label>
<input onChange={handleOnChange} name=”password”
placeholder=”queen_rox” size=”35" value={password}/>
<br/>
<input type=’submit’ value=’Submit’/>
</form>
);
};
From the definition of the LoginPage component, you can tell that this
component is different from the PublicPage component that we defined in the
Adding content to our React.js application section above. The PublicPage
While a class component is stateful and has a number of handy lifecycle methods
provided by React, a function component is just a JavaScript function which
makes it stateless. We can, and do, make the LoginPage component stateful in
the first line of its definition where we invoke a new addition to React as of
version 16.8, i.e. the useState hook. A ‘hook’ is a special function that lets us
hook into React features, e.g. the useState lets us hook into the state feature
which was previously exclusive to class components.
Note: The use of brackets in JavaScript syntax on the left-hand side of the
assignment is called ‘array destructuring’. It provides a more concise syntax for
allocating the results of a function, i.e. the first result of the function on the
right-hand side of the assignment populates the first variable in the array on the
left-hand side and so on.
So our code:
Because the useState hook returns an array of two items, the current value of
the component’s state and a function that we use to update it, our array only
requires two variables. These two variables achieve the same means as
this.state.* and this.setState() that we use in class components.
We use the four variables that we create with the useState hook in our
handleOnChange function. This function takes the input that the user enters into
the email and password fields of the form returned by our LoginPage component
and adds them to the state of the component. This gives us a way to store these
values so that we can submit them to our backend’s ‘/auth/login’ endpoint via
our handleSubmit function.
The handleSubmit function of our LoginPage component does the heavy lifting
for our application. First, it takes the email and password values from the
component’s state and sends them in a POST request to our Node.js backend.
Unfortunately, a React.js application can not natively send HTTP requests so we
need a means to achieve this. We could choose to build this functionality
ourselves using the XMLHttpRequest object that most browsers provide, but we
will use the axios library instead. The axios library enables us to send more
complex requests and is better at asynchronous communication using promises
rather than using events and callbacks. To use the axios library, we first install its
package.
We now have access to the post method that axios provides which we use to
send the POST request. If the request is invalid, our handleSubmit function
catches and returns the error. However, if the request is successful, it takes the
response which contains the JSON Web Token authenticating that the user is
who they claim to be, tells the browser to create an HTTP cookie (a.k.a. web
cookie or cookie), and stores the token in that cookie.
...
const BASE_URL = ‘http://localhost:4000';
...
ReactDOM.render(
<BrowserRouter>
<div>
<ul>
<li>
<Link to=’/public’>Public</Link>
</li>
<li>
<Link to=’/private’>Private</Link>
</li>
</ul>
<Switch>
<Route path=’/public’>
<PublicPage />
</Route>
<Route path=’/login’>
<LoginPage baseUrl={BASE_URL}/>
</Route>
<PrivateRoute path=’/private’>
<PrivatePage />
</PrivateRoute>
</Switch>
</div>
</BrowserRouter>
...
Within the Switch component we add a new Route component that will display
our LoginPage component when its route is requested. Next, we add a new
custom PrivateRoute component and define its route. This is a higher-order
component (HOC) that we will reuse to gate all of our private content. Let’s
define this new component now.
$ touch PrivateRoute.js
function PrivateRoute(props){
return(
<Route render={() => {
if(isAuthenticated()) {
return(props.children);
} else {
return(
<Redirect to={{pathname: ‘/login’, state: {from:
props.location}}}/>
);
}
}}/>
);
};
Like our LoginPage component from the Building a login component with
React.js and axios section above, our PrivateRoute component will also be a
‘function component’ that only takes props as an argument. In addition to being
a function component, our PrivateRoute component is also a higher-order
component. In React.js terminology, a higher-order component is a function that
takes a component as an argument and returns a new component, and that is
precisely what our PrivateRoute component is doing. In the Implementing
navigation and routing with React Router section above, we just nested the
component that we wanted rendered inside the Route component. However, the
Route component also has a property called render which takes a function as
argument. We use this function to execute logic to determine what component to
render: the component with our private content if the user is authenticated or
our login component if the user is unauthenticated. We have access to the
component with our private content via the props argument.
Within props is a property called children which contains any React elements
nested inside the component. Recall from earlier in this section that we nested a
component called PrivatePage within our PrivateRoute component.
...
<PrivateRoute path=’/private’>
<PrivatePage />
</PrivateRoute>
...
So, pending the results of an isAuthenticated function which we will define later
in this section, our logic within render tells our higher-order component to either
return our PrivatePage component or a Redirect component.
The Redirect component is provided by the React Router library and handles
more complex navigation. We can pass an object to this component’s to property
to define additional information like the pathname to redirect to, any query
strings that we want to include as parameters in the generated URL, and a state
to pass from the Redirect component to the component at the end of the path.
We provide the pathname property the path of our LoginPage component which
we defined as ‘/login’ earlier in this section.
Next, we provide the state property an object where we define a from property
as a pointer to the props.location object. In addition to a children property,
props also has a property called location which is an object that contains
information about the current URL that the user is on. We pass this information
to the Redirect component which in turn will pass it via the state property to
the component at the end of the path. In this case, that component is the
LoginPage component.
Recall the following code from the Building a login component with React.js
and axios section above where we defined our LoginPage component.
...
let location = useLocation();
let history = useHistory();
let {from} = location.state || { from: { pathname: “/” } };
...
history.push(from);
...
This code determines where the user is directed to from our LoginPage
component after logging in. To do this, we use the useLocation hook and the
useHistory hook provided by React Router. Calling the useLocation hook
returns an object with a pathname property that represents the path that the user
is currently on and a state property that represents the path that the user was
on right before the current URL.
Note: The use of braces in JavaScript syntax on the left-hand side of the
expression is called ‘object destructuring’. It provides a more concise syntax for
allocating the properties within an object, i.e. the variable inside the braces on
the left-hand side of the assignment will equal the value of the property with the
same name of the object on the right-hand side of the equation.
So our code:
Both segments of code will assign the variable from either indirectly to the value
of from within the state property of the location object if that value is truthy or
directly to an object literal.
So we know where we want to send the user, but we still need a means of
sending them there. We achieve this with the useHistory hook which, when
called, returns a history object that we can use for navigation. The history
object comes from the history package which was installed when we installed the
React Router package. This package provides a number of properties and
methods for managing session history in JavaScript including the push method.
This method takes a string representing the path to the new component, pushes
that as a new entry onto the “history stack”, and then loads the newly pushed
URL.
Note: The history stack is a stack of URLs. Recall from your Computer Science
days that a stack is a data structure in which the first data at the top of the stack
is the last data that was added to the stack. In the case of the history stack, we
are able to use the history object to push new URLs onto the top of the stack and
pop the last URL added from the top of the stack. This implementation of the
stack data structure makes it quick and easy for us to build navigation into our
web application.
By combining the useLocation hook and the useHistory hook provided by React
Router, we now know from which path the user ended up on the LoginPage
component and are able to direct the user accordingly once they are logged in. In
this scenario, the Redirect component will populate the state property for the
LoginPage component so that when the login form returned by our LoginPage
component is submitted the user will be redirected to the path that they were
trying to access, i.e. the ‘/private’ path.
So how will our PrivateRoute component know where to direct the user? This is
the job of the aforementioned isAuthenticated function which we will define
now. First, let’s create the file from which we will import this function.
$ touch isAuthenticated.js
The isAuthenticated function determines whether the user has access to the
content that they are requesting.
function isAuthenticated() {
var checkCookie = getCookie(‘my-token’);
if(checkCookie != null){
return true;
} else {
return false;
};
};
using a function called getCookie which we will define in the next section.
Depending on the results of the getCookie function, our isAuthenticated
function will either return true or false which our PrivateRoute component
will pick up and use to determine which component to return.
Retrieving data from the browsers by getting HTTP cookies with JavaScript
The isAuthenticated function defined in the Securing our routes with React
Router section above relies on a getCookie function. In this section, we define
that function. First, let’s create the file from which we will import this function.
$ touch getCookie.js
The getCookie function scans the HTTP cookies stored in the browser to find one
of a particular name. If that cookie exists, the function returns the value
associated with the cookie of that name. If that cookie does not exist, the
function returns a null value.
function getCookie(name){
var cookieArray = document.cookie.split(‘;’);
for(var i=0; i < cookieArray.length; i++){
var cookieKeyValuePair = cookieArray[i].split(‘=’);
if(name === cookieKeyValuePair[0].trim()) {
return(cookieKeyValuePair[1]);
};
};
return(null);
};
Recall from the Storing data in the browser by setting HTTP cookies with
JavaScript section above that JavaScript provides the document.cookie property.
Previously, we used this property to set a cookie, but it can also be used to get
cookies from the browser. These cookies are all returned as a single string
following the pattern ‘ <cookie-name>=<cookie-value>;’ We could write a
regular expression that parses through this string to find our cookie, but luckily
JavaScript has provided an easier method, the split method, to split a string
into an array of substrings based on the provided delimiter and return that array.
We store a reference to that array as a new cookieArray variable.
Now it’s a matter of looping through this array to find the element that we seek.
To do this, we split cookieArray into its own array, and store a reference to the
new array as a new cookieKeyValuePair variable. This array will only ever have
two elements consisting of ‘<cookie-name>’ at the index of 0 and ‘<cookie-
value>’ at the index of 1. It’s then a matter of comparing the value at the index
of 0 with the name that’s been provided to getCookie as an argument and when
a match is found, returning the value at the index of 1. Observe the use of the
trim method, also provided by JavaScript which I highly recommend to use as
there is hidden whitespace between the ‘;’ delimiter character between cookies
which must be trimmed in order to find a match.
Note: For those of you interested in ‘Big O Notation’, the algorithm that I have
used for this function has a runtime of O(n). This makes it a ‘linear-time
algorithm’ which will grow with the number of items in the array in the worst
case scenario.
To put all of this into the context of our React.js application, our isAuthenticated
function calls our getCookie function and passes to it ‘my-token’ which is what
we named the cookie in the Building a login component with React.js and
axios section above. The getCookie function checks whether this cookie exists
and returns the value of the cookie which is the value of the JSON Web Token set
in our LoginPage component. Our isAuthenticated function checks this value,
returns true , and the user is granted access to the private content secured by our
To secure additional routes, we can simply add each path to Switch as a new
PrivateRoute higher-order component.
...
<Switch>
...
<PrivateRoute path=’/private’>
<PrivatePage />
</PrivateRoute>
<PrivateRoute path=’/superprivate’>
<SuperPrivatePage />
</PrivateRoute>
...
</Switch>
...
Any request to these paths will also redirect the user to our LoginPage
$ touch PrivatePage.js
This component sends a GET request to the ‘/items’ endpoint that we defined in
our Node.js application in order to fetch the data to populate the elements in the
returned list. The response from our Node.js application depends on our route.
...
app.route(‘/items’)
.post(addItem)
.get(getItems);
...
Coded this way, our backend would respond with a status code of 200 OK and
the requested data for any request to this route.
But this means that any client that can send HTTP requests could send a GET
request to our ‘/items’ endpoint and receive this data. That is why in Building a
RESTful API with Node.js we added the verifyToken middleware function to the
route.
...
app.route(‘/items’)
.post(addItem)
.get(verifyToken, getItems);
...
Without any changes in our React.js application, our backend would now
respond with a status code of 403 Forbidden and an error message to our request
to this route.
The good news is that this means that our verifyToken middleware function is
working, and our backend now expects a JSON Web Token to be provided. To get
this working again, we need to add the token to the segment of our code in the
frontend where we send the GET request to the ‘/items’ endpoint. This code
exists in our updateState function.
...
updateState = () => {
let token = getCookie(‘my-token’);
axios.get(‘http://localhost:4000/items', {headers: {‘x-json-web-
token’: token}})
.then((res) => {
this.setState({items: res.data});})
.catch((error) => {
console.log(error);
});
};
...
We use the getCookie function that we defined in the Retrieving data from the
browsers by getting HTTP cookies with JavaScript section above to retrieve
the token that is stored in the browser’s cookies from when the user logged in.
We then pass a new parameter to the post method provided by the axios library
which inserts a new HTTP header into our request with the provided name and
the value of the token returned by the call to our getCookie function. We provide
the new header with the name x-json-web-token because that is the name we
told our verifyToken middleware function to find the token. Now when our
frontend submits the GET request it receives a response with a status code of 200
Notice that the Request Headers section has a new entry with our x-json-web-
We can reuse this paradigm across our frontend for future requests to the
backend where we want to secure the request with token verification. In fact, we
could even refactor our React.js application to create a function for this paradigm
that we can import and call from anywhere in our frontend, but that is a subject
for another blog!
component our browser will throw an error. The error itself will vary by browser.
//Google Chrome
Access to XMLHttpRequest at ‘http://localhost:4000/auth/login' from
origin ‘http://localhost:3000' has been blocked by CORS policy:
Response to preflight request doesn’t pass access control check: No
‘Access-Control-Allow-Origin’ header is present on the requested
resource.
//Mozilla Firefox
Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at http://localhost:4000/auth/login. (Reason:
header ‘content-type’ is not allowed according to header ‘Access-
Control-Allow-Headers’ from CORS preflight response).
...
app.use(function(req, res, next) {
res.header(“Access-Control-Allow-Origin”, “*”);
res.header(“Access-Control-Allow-Headers”, “Content-Type”);
next();
});
...
We pass a function as middleware that sets two headers on the response enabling
our backend to share resources with our frontend. The first header is the Access-
means that our backend can share resources with any domain on the internet.
The second header is the Access-Control-Allow-Headers header which allows our
backend to specify which HTTP headers it will accept in the request. When
sending an HTTP request, the browser first sends a ‘preflight’ request using the
OPTIONS HTTP method that lets the backend server know which HTTP headers
might be sent in the actual request. The backend responds to this preflight
request with a preflight response. The preflight request contains the Access-
Host: localhost:4000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:76.0)
Gecko/20100101 Firefox/76.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Referer: http://localhost:3000/login
Origin: http://localhost:3000
Connection: keep-alive
The addition of the two headers in our index.j s file will generate the following
preflight response.
HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type, x-json-web-token
Allow: POST
Content-Type: text/html; charset=utf-8
Content-Length: 4
ETag: W/”4-Yf+Bwwqjx254r+pisuO9HfpJ6FQ”
Connection: keep-alive
Now if we started our React.js application and tried to log in using the LoginPage
As Webpack parses through our application, it will come across code where we
have used modern JavaScript syntax which became available in the sixth version
of ECMAScript (a.k.a. ES2015 or ES6). Unfortunately support for ES6 is
inconsistent across browsers and browser versions, and, as mentioned earlier in
this section, we want end users of our application to have the same experience
regardless of their browser of choice. To ensure that our application will run
consistently and smoothly across browsers, we need to transpile our code from
ES6 down to a version of JavaScript that is widely supported across browsers.
One tool to perform this transpilation is Babel which we can install in our project
via the babel package.
Now we could set up our own build environment by installing each of the
necessary tools and build a script to tie them all together, or we could install just
the react-scripts package which does all of this for us including installing
babel and webpack as dependencies.
The react-scripts package contains a start.js file which, when executed, will
run a script to start our React.js application. We want to run this script via our
command line interface, but JavaScript files are not executable from the
command line. Instead, we use the Node package manager. Throughout this blog
series, we have only used npm to install packages that we want to use into our
project, however npm also provides an interface to work with them. For example,
the npm start command can be used to run scripts specified in our project. The
only catch is that this command looks for these scripts in a file called the
package.json file. Let’s create this file now.
$ touch package.json
{
“name”: “rest-api-node-react”,
“scripts”: {
“start”: “react-scripts start”
},
...
The package.json file has many uses such as managing dependencies, but for our
purposes we use it to interface with the react-scripts package by setting the
start script in the file’s scripts property to react-scripts start . The scripts
property is a dictionary containing script commands that are run at various times
throughout the lifecycle of our application. The key is the lifecycle event (e.g.
start , build , and eject ), and the value is the command to run at that point
Doing this means that when we run npm start the script within the start.js file
will be executed.
$ npm start
directory and our index.js file and associated components in a src/ folder. Let’s
create these directories.
$ mkdir public
$ mkdir src
Once we have moved the files into the appropriate directories, let’s try running
our application again.
$ npm start
It works! Under the covers, the start.js script invokes Webpack to parse
through the application. Webpack starts parsing at src/index.js and loads any
imported modules until it has a complete dependency graph. When it comes
across ES6 code, Webpack passes these files to Babel to convert the code into a
version of JavaScript that will behave consistently across browsers. Webpack
uses the dependency graph to generate a single JavaScript file consisting of our
application’s source code, injects the file via a script tag into public/index.html ,
We now have a frontend built with the React.js library that secures our Node.js
backend from unauthorized requests!
So to retrospect on the work that we did in this blog, we first restructured the
project that we have been working on throughout my previous blogs Building a
RESTful API and Securing a RESTful API Built with Node.js to clearly delineate
our frontend code from our backend code and migrated all of the frontend code
from that blog according to this new structure. We then built out our frontend
starting with a basic React.js application and adding components to present
publicly accessible content. Next, we added new components to present private
content only accessible to authorized users and built a login component to
enable users to authorize themselves. We then added navigation between our
public and private content using the React Router library and secured all routes
to private content against unauthorized users by redirecting them to our login
component. Finally, we enabled communication between the frontend and the
backend using the axios library and secured that requests to the backend so that
only authorized users could access the data protected in our backend via
middleware.
In his spare time, Colin can be found checking out the latest Alite camp kit for a
weekend away in Big Sur, planning his next line down a mountain at Kirkwood,
or surfing the Horror section on Netflix. Colin is currently located in San
Francisco, California.
67 2
A monthly summary of the best stories shared in Level Up Coding Take a look.