⟵back

Securing a React app with Express

It's a common misconception that frontend apps are inherently secure; that since they aren't attached to a database therefore there's nothing to really hack. Well, I'm very glad I found out I was wrong before deploying my frontend project that consumes a private API!

Up until then, I'd been storing information like API keys as environmental variables in an .env file and loading these directly into my React components as process.env.VARIABLE_NAME. Unfortunately, this exposes the values to anyone who knows their way around the source code of a web page, as demonstrated here. I was shook, I tell you!

Instead, what we can do is cover up these raw API calls with a Node.js proxy server using the Express framework. This will loosely follow the tutorial in the video I just linked — I just changed a couple of things for my own purposes.

First of all, you'll need to install some extra packages to your existing project. Run the following:

npm i axios cors dotenv express nodemon

Then, in the root of your project, create a file called server.js and fill it with the following:

// the port you choose is up to you; just make sure it's not already in use
const PORT =  8000;

// Axios is your client for making API requests
const axios =  require("axios");

// CORS basically allows your frontend & backend to access each other securely
const cors =  require("cors");

// Express is a backend framework for Node.js
const express =  require("express");

// create an app object
const app =  express()

// this is just an extra so your JSON response will be formatted readably
app.set("json spaces",  2)

// with Dotenv you access your environment variables
require("dotenv").config()

Now that you've added a port, you can set up the server. Go to package.json and add a new line at the top, before the dependencies section:

{

    "proxy":  "http://localhost:8000",

    ...

}

Add another line to the scripts section:

"scripts": {

	...

	"start:backend":  "nodemon index.js",

	...

},

Before going any further, we need to do something very important: create an .env file in the project root. In here, add the following:

REACT_APP_JWT_TOKEN=access_token_for_rest_api_in_string_format
REACT_APP_API_URL=root_url_for_rest_api_in_string_format

Note the importance of prefixing variables with REACT_APP_ to ensure that they get imported!

Now go back to server.js — it's now time to write your first request.

Let's create an endpoint called /restaurants, which gives you a list of restaurants. Using app.get() produces a GET request, which we will store in an object called getRestaurantList. You can see the requisite headers in the example below. What might now stand out is process.env.REACT_APP_API_URL, which I touched on at the beginning of the post. This fetches REACT_APP_API_URL from the process.env variable executed at runtime.

The axios request section doesn't differ too wildly from its equivalent within a React component. Just remember that unlike in React, using axios is a must here, as the JS fetch method we used with React is not compatible with Node.js (which the Express backend framework is based on).

const PORT =  8000;
const axios =  require("axios");
const cors =  require("cors");
const express =  require("express");
const app =  express()

app.set("json spaces",  2)
require("dotenv").config()

app.get("/restaurants", (req, res) => {

    const getRestaurantList = {
        url: `${process.env.REACT_APP_API_URL}/restaurants`,
        headers: {
            "Authorization": `JWT ${process.env.REACT_APP_JWT_TOKEN}`,
            "Accept": "application/json",
            "Content-Type": "application/json"
        }
    }

    axios.request(getRestaurantList).then((response) => {
        res.json(response.data)
        }).catch((error)  =>  {
        console.error(error)
    })

});

Now, in your console, run npm run start:backend. If it doesn't open automatically, open a new tab in your browser and visit http://localhost:8000/restaurants/. You should see some raw JSON from your API.

So how do we now make those calls, via our proxy, from the frontend? It's actually pretty similar to before. Let's refer to this post — specifically, the RestaurantList component.

import React, { Component } from "react"; 
import { Link } from "react-router-dom"; 
import { REACT_APP_JWT_TOKEN, REACT_APP_API_URL } from "../constants"; 


function RestaurantList() {
    const [restaurantList, setRestaurants] = useState([]); 
    const [selectedRestaurant, setSelectedRestaurant] = useState(null); 
    useEffect(() => {
        (async () => {
            const response = await fetch(`${REACT_APP_API_URL}restaurants/`, { 
                method: "GET", 
                headers: {
                    "Authorization": `JWT ${REACT_APP_JWT_TOKEN}`, 
                    "Accept" : "application/json", 
                    "Content-Type": "application/json" 
                }
            }); 
            const data = await response.json(); 
            setRestaurants(data); 
            })(); 
        }, []); 
    ...

Note the import statement at the top, import { REACT_APP_JWT_TOKEN, REACT_APP_API_URL } from "../constants";, which I've since found out is a bad practice. Since the variables are being loaded from the environment via the proxy we'll be requesting, we can delete this import statement.

Now all we need to do is change is the request itself —starting from the invocation of useEffect() — so that it looks like this:

useEffect(() => {
    (async () => {
        const response = await fetch("/restaurants"); 
        const data = await response.json(); 
        setRestaurants(data); 
    })(); 
}, []); 

Doesn't that look much cleaner? That was the point, too; separating concerns and concealing sensitive information.

You can add more requests to the server.js file. If you need to pass in a parameter to your URL — which I went into in this post — you can access it with req.url, which grabs the query string suffix (represented in this example as :slug in the endpoint definition).

So, if your API endpoint happened to be /api/v1/restaurants/yummy-restaurant, the req.url is whatever comes after the last slash: yummy-restaurant. You can see below how to use string formatting to put these two variables together to form the whole endpoint.

app.get("/restaurants/:slug", (req, res) => {

    const getRestaurantInstance = {
        method: "GET",
        url: `${process.env.REACT_APP_API_URL}${req.url}`,
        headers: {
            "Authorization": `JWT ${process.env.REACT_APP_JWT_TOKEN}`,
            "Accept" : "application/json",
            "Content-Type": "application/json"
        }
    }

    axios.request(getRestaurantInstance).then((response) => {
        res.json(response.data)
        }).catch((error) => {
        console.error(error)
    })

});

Then, in the React component:

function RestaurantInstance() {

    let { restaurant_slug } = useParams();
    const restaurantSlug = restaurant_slug;
    const [restaurantInstance, setrestaurantInstance] = useState(null);

    useEffect(() => {

        if (!restaurantSlug) {
            return;
        }

        (async () => {
            const response = await fetch(`/restaurants/${restaurantSlug}`);
            const restaurantInstance = await response.json();
            setRestaurantInstance(restaurant);
        })();

    }, [restaurantSlug]);
    
    ...

Hopefully it goes without saying that these components are incomplete 😉 if you try to copy-paste them and run it, you'll get an error. I haven't included the return statement here because how it's rendered is not relevant; the only thing that's changed is the way we obtain the data.