Deploying a Multi-Route React App: Combining Express and Static Build

Marwan Zaarab
5 min readApr 2, 2023

Introduction

Deploying a React application can be a daunting task, especially when it comes to serving multiple routes from a Node/Express server. Fortunately, with the help of a few libraries and some careful configuration, it’s possible to easily serve your multi-route React application using a static build folder.

In this tutorial, we’ll cover the steps involved in deploying such an application. To simplify things, we’ll assume that you already have a React project and a corresponding backend project with API routes configured. Throughout this tutorial, we’ll refer to these directories as blogs-frontend and blogs-backend respectively.

Dependencies we will use

  • express — routing, middleware, and to serve static files like images, CSS and JavaScript files.
  • cors — will allow us to make requests from any domain. We will require and add it as middleware to our Express application
  • http-proxy — is a third-party Node.js module that we will use to create an HTTP proxy server that will act as an intermediary between the client and the backend server. This approach allows us to avoid CORS-related issues as well as use relative paths in our client-side code. In doing so, we can effectively hide the backend server from the client, and ensure that requests are correctly routed to the appropriate endpoint.
  • http — built-in Node.js module that provides an HTTP server and client. We will use it to create a web server that can make HTTP requests to other servers.
  • path — another built-in Node.js module that allows us to work with file and directory paths.

The inclusion of http-proxy in this tutorial was initially considered to allow multiple paths in the URL and act as an intermediary between the client and the backend server. This approach can be useful in more complex scenarios where you might want to proxy requests to different backend services based on the URL path. However, for the purposes of this tutorial, using a proxy server might be overcomplicating things. You can achieve the same results (avoiding CORS issues and using relative paths) by configuring your Express server correctly.

Preparing the React Application

  1. If you haven’t already, run npm run build from the root of your React project directory (i.e., blogs-frontend) to create a production-ready build of your React app.
  2. Once it’s finished, navigate to your backend directory (i.e., blogs-backend) and copy the build folder to your backend directory.

The way I like to do this is by adding the following script in package.json:

"scripts": {
// ...
"build:cp": "rm -rf build && cd ../blogs-frontend && npm run build && cp -r build ../blogs-backend",
// ...
}

Then, you can simply run the following command from the terminal:

npm run build:cp

This will delete your previous build, create a new one and then copy it to your backend directory.

Setting up the Node/Express Backend

1. Create the app or server file for your backend (if you haven’t already)

From the root directory of your backend project, create a file called server.js and install the following 3 dependencies:

npm install --save express cors http-proxy

2. Set up the Express Server

Now that we have all the dependencies we need, let’s start by requiring them and creating our Express server:

require("dotenv").config()
const express = require('express');
const path = require('path');
const cors = require("cors");
const app = express();

app.use(cors()); // Enable CORS to allow requests from any domain
app.use(express.json()); // Parse incoming JSON payloads

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
  • In this example, the Express server listens on port 3001. The express.json() middleware is a built-in middleware in Express that parses incoming JSON payloads and makes them available in req.body. Without this middleware, your application would not be able to parse incoming JSON payloads and would return an error.
  • By calling require("dotenv").config(), the environment variables defined in our .env file are loaded into the process.env object and made available to your application. If you're not using environment variables, you can omit this part.

Mounting and Routing all endpoints

We will continue by mounting all of our defined API routes. Assuming we have defined our API routes in a separate folder (e.g. in a routes folder with blogs-routes.js and users-routes.js), we would mount them like this:

app.use("/api/blogs", blogsRoutes);
app.use("/api/users", usersRoutes);

Then, we will instruct express to use our build folder to serve all of our static CSS, images and JavaScript files:

app.use(express.static("build"));

Finally, we’ll add a catch-all route to handle client-side routing and serve our index.html file. This route will render your React homepage as well as any subroutes defined within your BrowserRouter or Switch.

app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "build", "index.html"));
});

You can use path.join or path.resolve, both achieve the same thing and will send the file located at blogs-backend/build/index.html.

🚨 Keep in mind that the order in which you mount your routes/routers matters, so make sure to mount all API routes before serving your static build folder and the catch-all route to avoid conflicts.

The final code should look like this:

require("dotenv").config();
const express = require('express');
const path = require('path');
const cors = require("cors");
const blogsRoutes = require("./routes/blogs-routes");
const usersRoutes = require("./routes/users-routes");

const app = express();
app.use(cors());
app.use(express.json());

app.use("/api/blogs", blogsRoutes);
app.use("/api/users", usersRoutes);

app.use(express.static("build"));
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "build", "index.html"));
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Unknown endpoints (optional)

  • You probably noticed that since the catch-all route (app.get("*")) matches all routes, it will handle any unknown endpoint and override any route you may have defined to handle them.
  • To overcome this issue, you could define separate routes for each of your React sub-routes, like this:
app.use(express.static("build"));

const indexPath = path.resolve(__dirname, "build", "index.html");
app.get("/blogs", (req, res) => res.sendFile(indexPath));
app.get("/blogs/new", (req, res) => res.sendFile(indexPath));
app.get("/blogs/:id", (req, res) => res.sendFile(indexPath));
app.get("/users", (req, res) => res.sendFile(indexPath));
app.get("/users/new", (req, res) => res.sendFile(indexPath));
app.get("/users/:id", (req, res) => res.sendFile(indexPath));

app.get("*", (req, res) => {
res.status(404).send({ error: "unknown endpoint" });
});
  • By doing this, the catch-all route will handle any unknown routes after checking all of your routes (API routes and frontend routes).
  • It’s also not serving index.html anymore in this case but simply returning a 404 response instead for all unknown endpoints and undefined React routes.
  • You can choose to redirect users to your homepage instead by replacing that line with res.redirect("/")

Summary

That’s it! By following these steps, you should now have a fully functioning Node/Express server serving your static React build. By using a static build folder, you can serve your app’s assets more efficiently and reduce the load on your server.

--

--